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 +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +41 -0
- data/Rakefile +11 -0
- data/bin/heroku-scalr +7 -0
- data/heroku-scalr.gemspec +29 -0
- data/lib/heroku/scalr/app.rb +123 -0
- data/lib/heroku/scalr/cli.rb +57 -0
- data/lib/heroku/scalr/config.rb +33 -0
- data/lib/heroku/scalr/runner.rb +35 -0
- data/lib/heroku/scalr.rb +32 -0
- data/spec/fixtures/config_a.rb +9 -0
- data/spec/heroku/scalr/app_spec.rb +104 -0
- data/spec/heroku/scalr/config_spec.rb +17 -0
- data/spec/heroku/scalr/runner_spec.rb +13 -0
- data/spec/heroku/scalr_spec.rb +4 -0
- data/spec/spec_helper.rb +19 -0
- metadata +176 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
data/bin/heroku-scalr
ADDED
@@ -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
|
data/lib/heroku/scalr.rb
ADDED
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|