heroku-scalr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: