heroku-scalr 0.1.0 → 0.2.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/.gitignore +1 -0
- data/heroku-scalr.gemspec +1 -1
- data/lib/heroku/scalr/app.rb +12 -15
- data/lib/heroku/scalr/metric.rb +81 -0
- data/lib/heroku/scalr.rb +2 -1
- data/spec/heroku/scalr/app_spec.rb +41 -41
- data/spec/heroku/scalr/metric_spec.rb +132 -0
- metadata +5 -2
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/heroku-scalr.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = File.basename(__FILE__, '.gemspec')
|
8
8
|
s.summary = "Watch and scale your dynos!"
|
9
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.
|
10
|
+
s.version = "0.2.0"
|
11
11
|
|
12
12
|
s.authors = ["Black Square Media"]
|
13
13
|
s.email = "info@blacksquaremedia.com"
|
data/lib/heroku/scalr/app.rb
CHANGED
@@ -6,11 +6,15 @@ class Heroku::Scalr::App
|
|
6
6
|
max_dynos: 2,
|
7
7
|
wait_low: 10,
|
8
8
|
wait_high: 100,
|
9
|
+
ping_low: 200,
|
10
|
+
ping_high: 500,
|
11
|
+
metric: :ping,
|
9
12
|
min_frequency: 60
|
10
13
|
}.freeze
|
11
14
|
|
12
15
|
attr_reader :name, :http, :api, :interval, :min_dynos, :max_dynos,
|
13
|
-
:wait_low, :wait_high, :
|
16
|
+
:metric, :wait_low, :wait_high, :ping_low, :ping_high,
|
17
|
+
:min_frequency, :last_scaled_at
|
14
18
|
|
15
19
|
# @param [String] name Heroku app name
|
16
20
|
# @param [Hash] opts options
|
@@ -45,6 +49,9 @@ class Heroku::Scalr::App
|
|
45
49
|
@max_dynos = opts[:max_dynos].to_i
|
46
50
|
@wait_low = opts[:wait_low].to_i
|
47
51
|
@wait_high = opts[:wait_high].to_i
|
52
|
+
@ping_low = opts[:ping_low].to_i
|
53
|
+
@ping_high = opts[:ping_high].to_i
|
54
|
+
@metric = Heroku::Scalr::Metric.new(opts[:metric], self)
|
48
55
|
@min_frequency = opts[:min_frequency].to_i
|
49
56
|
@last_scaled_at = Time.at(0)
|
50
57
|
end
|
@@ -57,20 +64,7 @@ class Heroku::Scalr::App
|
|
57
64
|
return
|
58
65
|
end
|
59
66
|
|
60
|
-
|
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
|
67
|
+
do_scale(metric.by)
|
74
68
|
end
|
75
69
|
|
76
70
|
# @param [Symbol] level
|
@@ -81,12 +75,15 @@ class Heroku::Scalr::App
|
|
81
75
|
|
82
76
|
protected
|
83
77
|
|
78
|
+
|
84
79
|
# @return [Time] the next scale attempt
|
85
80
|
def next_scale_attempt
|
86
81
|
last_scaled_at + min_frequency
|
87
82
|
end
|
88
83
|
|
89
84
|
def do_scale(by)
|
85
|
+
return if by.zero?
|
86
|
+
|
90
87
|
info = api.get_app(name)
|
91
88
|
unless info.status == 200
|
92
89
|
log :warn, "error fetching app info, responded with #{info.status}"
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Heroku::Scalr::Metric
|
2
|
+
|
3
|
+
# @param [Symbol] type the metric type
|
4
|
+
# @param [Heroku::Scalr::App] app the application
|
5
|
+
# @return [Heroku::Scalr::Metric::Abstract] a metric instance
|
6
|
+
def self.new(type, app)
|
7
|
+
case type
|
8
|
+
when :wait, "wait"
|
9
|
+
Wait.new(app)
|
10
|
+
else
|
11
|
+
Ping.new(app)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Abstract
|
16
|
+
|
17
|
+
# @param [Heroku::Scalr::App] app the application
|
18
|
+
def initialize(app)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Integer] number of dynos to adjust by
|
23
|
+
def by
|
24
|
+
0
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def compare(ms, low, high)
|
30
|
+
ms <= low ? -1 : (ms >= high ? 1 : 0)
|
31
|
+
end
|
32
|
+
|
33
|
+
def log(*args)
|
34
|
+
@app.log(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class Ping < Abstract
|
40
|
+
|
41
|
+
# @see Heroku::Scalr::Metric::Abstract#by
|
42
|
+
def by
|
43
|
+
status = nil
|
44
|
+
|
45
|
+
real = Benchmark.realtime do
|
46
|
+
status = @app.http.get.status
|
47
|
+
end
|
48
|
+
|
49
|
+
unless status == 200
|
50
|
+
log :warn, "unable to ping, server responded with #{status}"
|
51
|
+
return 0
|
52
|
+
end
|
53
|
+
|
54
|
+
ms = (real * 1000).floor
|
55
|
+
log :debug, "current ping time: #{ms}ms"
|
56
|
+
|
57
|
+
compare(ms, @app.ping_low, @app.ping_high)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class Wait < Abstract
|
63
|
+
|
64
|
+
# @see Heroku::Scalr::Metric::Abstract#by
|
65
|
+
def by
|
66
|
+
ms = @app.http.get.headers["X-Heroku-Queue-Wait"]
|
67
|
+
unless ms
|
68
|
+
log :warn, "unable to determine queue wait time"
|
69
|
+
return 0
|
70
|
+
end
|
71
|
+
|
72
|
+
ms = ms.to_i
|
73
|
+
log :debug, "current queue wait time: #{ms}ms"
|
74
|
+
|
75
|
+
compare(ms, @app.wait_low, @app.wait_high)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
end
|
data/lib/heroku/scalr.rb
CHANGED
@@ -2,6 +2,7 @@ require 'heroku/api'
|
|
2
2
|
require 'logger'
|
3
3
|
require 'excon'
|
4
4
|
require 'timers'
|
5
|
+
require 'benchmark'
|
5
6
|
|
6
7
|
module Heroku::Scalr
|
7
8
|
extend self
|
@@ -27,6 +28,6 @@ module Heroku::Scalr
|
|
27
28
|
|
28
29
|
end
|
29
30
|
|
30
|
-
%w|config app runner|.each do |name|
|
31
|
+
%w|config app runner metric|.each do |name|
|
31
32
|
require "heroku/scalr/#{name}"
|
32
33
|
end
|
@@ -2,21 +2,24 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Heroku::Scalr::App do
|
4
4
|
|
5
|
-
subject { described_class.new('name', api_key: 'key') }
|
5
|
+
subject { described_class.new('name', api_key: 'key', max_dynos: 3) }
|
6
6
|
|
7
7
|
def mock_response(status, body)
|
8
8
|
mock "APIResponse", status: status, headers: {}, body: body
|
9
9
|
end
|
10
10
|
|
11
|
-
its(:name) { should == 'name' }
|
11
|
+
its(:name) { should == 'name' }
|
12
12
|
its(:http) { should be_instance_of(Excon::Connection) }
|
13
13
|
its(:api) { should be_instance_of(Heroku::API) }
|
14
|
+
its(:metric) { should be_instance_of(Heroku::Scalr::Metric::Ping) }
|
14
15
|
|
15
16
|
its(:interval) { should be(30) }
|
16
17
|
its(:min_dynos) { should be(1) }
|
17
|
-
its(:max_dynos) { should be(
|
18
|
+
its(:max_dynos) { should be(3) }
|
18
19
|
its(:wait_low) { should be(10) }
|
19
20
|
its(:wait_high) { should be(100) }
|
21
|
+
its(:ping_low) { should be(200) }
|
22
|
+
its(:ping_high) { should be(500) }
|
20
23
|
its(:min_frequency) { should be(60) }
|
21
24
|
its(:last_scaled_at) { should == Time.at(0)}
|
22
25
|
|
@@ -39,66 +42,63 @@ describe Heroku::Scalr::App do
|
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
|
-
describe "
|
45
|
+
describe "scaling" do
|
46
|
+
before do
|
47
|
+
subject.api.stub get_app: mock_response(200, { "dynos" => 2 }), put_dynos: mock_response(200, "")
|
48
|
+
subject.metric.stub by: -1
|
49
|
+
end
|
43
50
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
51
|
+
it "should skip if scaled too recently" do
|
52
|
+
subject.instance_variable_set :@last_scaled_at, Time.now
|
53
|
+
subject.scale!.should be_nil
|
54
|
+
end
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
56
|
+
it "should determine scale through metric" do
|
57
|
+
subject.metric.should_receive(:by).and_return(-1)
|
58
|
+
subject.scale!.should == 1
|
59
|
+
end
|
53
60
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
it "should skip when there is no need" do
|
62
|
+
subject.metric.should_receive(:by).and_return(0)
|
63
|
+
subject.scale!.should be_nil
|
64
|
+
end
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 2 })
|
66
|
-
subject.scale!.should == 1
|
67
|
-
end
|
66
|
+
it "should check current number of dynos" do
|
67
|
+
subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 2 })
|
68
|
+
subject.scale!.should == 1
|
69
|
+
end
|
70
|
+
|
71
|
+
context "down" do
|
68
72
|
|
69
|
-
it "should
|
73
|
+
it "should return the new number of dynos" do
|
70
74
|
subject.api.should_receive(:put_dynos).with("name", 1).and_return mock_response(200, "")
|
71
75
|
subject.scale!.should == 1
|
72
76
|
end
|
73
77
|
|
74
|
-
it "should
|
78
|
+
it "should skip if min number of dynos reached" do
|
75
79
|
subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 1 })
|
76
80
|
subject.api.should_not_receive(:put_dynos)
|
77
81
|
subject.scale!.should be_nil
|
78
82
|
end
|
83
|
+
|
79
84
|
end
|
80
85
|
|
81
|
-
context "
|
82
|
-
before do
|
83
|
-
subject.api.stub get_app: mock_response(200, { "dynos" => 1 }), put_dynos: mock_response(200, "")
|
84
|
-
end
|
86
|
+
context "up" do
|
85
87
|
|
86
|
-
|
87
|
-
stub_request(:get, "http://name.herokuapp.com/robots.txt").
|
88
|
-
to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 101 })
|
89
|
-
end
|
88
|
+
before { subject.metric.stub by: 1 }
|
90
89
|
|
91
|
-
it "should
|
92
|
-
subject.api.should_receive(:
|
93
|
-
subject.scale!.should ==
|
90
|
+
it "should return the new number of dynos" do
|
91
|
+
subject.api.should_receive(:put_dynos).with("name", 3).and_return mock_response(200, "")
|
92
|
+
subject.scale!.should == 3
|
94
93
|
end
|
95
94
|
|
96
|
-
it "should
|
97
|
-
subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" =>
|
95
|
+
it "should skip if max number of dynos reached" do
|
96
|
+
subject.api.should_receive(:get_app).with("name").and_return mock_response(200, { "dynos" => 3 })
|
98
97
|
subject.api.should_not_receive(:put_dynos)
|
99
98
|
subject.scale!.should be_nil
|
100
99
|
end
|
100
|
+
|
101
101
|
end
|
102
|
-
end
|
103
102
|
|
103
|
+
end
|
104
104
|
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Scalr::Metric do
|
4
|
+
|
5
|
+
let(:app) { Heroku::Scalr::App.new('name', api_key: 'key') }
|
6
|
+
|
7
|
+
it 'should create metric instances' do
|
8
|
+
described_class.new(:wait, app).should be_instance_of(described_class::Wait)
|
9
|
+
described_class.new("wait", app).should be_instance_of(described_class::Wait)
|
10
|
+
described_class.new(:ping, app).should be_instance_of(described_class::Ping)
|
11
|
+
described_class.new(:any, app).should be_instance_of(described_class::Ping)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
describe Heroku::Scalr::Metric::Abstract do
|
17
|
+
|
18
|
+
let(:app) { Heroku::Scalr::App.new('name', api_key: 'key') }
|
19
|
+
subject { described_class.new(app) }
|
20
|
+
its(:by) { should == 0 }
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe Heroku::Scalr::Metric::Ping do
|
25
|
+
|
26
|
+
let(:app) { Heroku::Scalr::App.new('name', api_key: 'key') }
|
27
|
+
let(:ping_time) { 0.250 }
|
28
|
+
let!(:http_request) { stub_request(:get, "http://name.herokuapp.com/robots.txt") }
|
29
|
+
|
30
|
+
subject { described_class.new(app) }
|
31
|
+
before { Benchmark.stub(:realtime).and_yield.and_return(ping_time) }
|
32
|
+
|
33
|
+
describe "low ping time" do
|
34
|
+
let(:ping_time) { 0.150 }
|
35
|
+
|
36
|
+
it 'should scale down' do
|
37
|
+
subject.by.should == -1
|
38
|
+
http_request.should have_been_made
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "high ping time" do
|
43
|
+
let(:ping_time) { 0.550 }
|
44
|
+
|
45
|
+
it 'should scale up' do
|
46
|
+
subject.by.should == 1
|
47
|
+
http_request.should have_been_made
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "normal ping time" do
|
52
|
+
it 'should not scale' do
|
53
|
+
subject.by.should == 0
|
54
|
+
http_request.should have_been_made
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "failed requests" do
|
59
|
+
|
60
|
+
let! :http_request do
|
61
|
+
stub_request(:get, "http://name.herokuapp.com/robots.txt").to_return(status: 404)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not scale' do
|
65
|
+
Heroku::Scalr.logger.should_receive(:warn)
|
66
|
+
subject.by.should == 0
|
67
|
+
http_request.should have_been_made
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
describe Heroku::Scalr::Metric::Wait do
|
75
|
+
|
76
|
+
let(:app) { Heroku::Scalr::App.new('name', api_key: 'key') }
|
77
|
+
subject { described_class.new(app) }
|
78
|
+
|
79
|
+
describe "low queue wait time" do
|
80
|
+
|
81
|
+
let! :http_request do
|
82
|
+
stub_request(:get, "http://name.herokuapp.com/robots.txt").to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 3 })
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should scale down' do
|
86
|
+
subject.by.should == -1
|
87
|
+
http_request.should have_been_made
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "high queue wait time" do
|
93
|
+
|
94
|
+
let! :http_request do
|
95
|
+
stub_request(:get, "http://name.herokuapp.com/robots.txt").to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 300 })
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should scale up' do
|
99
|
+
subject.by.should == 1
|
100
|
+
http_request.should have_been_made
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "normal queue wait time" do
|
106
|
+
|
107
|
+
let! :http_request do
|
108
|
+
stub_request(:get, "http://name.herokuapp.com/robots.txt").to_return(body: "", headers: { "X-Heroku-Queue-Wait" => 20 })
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should not scale' do
|
112
|
+
subject.by.should == 0
|
113
|
+
http_request.should have_been_made
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "queue wait unretrievable" do
|
119
|
+
|
120
|
+
let! :http_request do
|
121
|
+
stub_request(:get, "http://name.herokuapp.com/robots.txt")
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should not scale' do
|
125
|
+
Heroku::Scalr.logger.should_receive(:warn)
|
126
|
+
subject.by.should == 0
|
127
|
+
http_request.should have_been_made
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroku-scalr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: timers
|
@@ -131,6 +131,7 @@ executables:
|
|
131
131
|
extensions: []
|
132
132
|
extra_rdoc_files: []
|
133
133
|
files:
|
134
|
+
- .gitignore
|
134
135
|
- .travis.yml
|
135
136
|
- Gemfile
|
136
137
|
- Gemfile.lock
|
@@ -141,10 +142,12 @@ files:
|
|
141
142
|
- lib/heroku/scalr/app.rb
|
142
143
|
- lib/heroku/scalr/cli.rb
|
143
144
|
- lib/heroku/scalr/config.rb
|
145
|
+
- lib/heroku/scalr/metric.rb
|
144
146
|
- lib/heroku/scalr/runner.rb
|
145
147
|
- spec/fixtures/config_a.rb
|
146
148
|
- spec/heroku/scalr/app_spec.rb
|
147
149
|
- spec/heroku/scalr/config_spec.rb
|
150
|
+
- spec/heroku/scalr/metric_spec.rb
|
148
151
|
- spec/heroku/scalr/runner_spec.rb
|
149
152
|
- spec/heroku/scalr_spec.rb
|
150
153
|
- spec/spec_helper.rb
|