heroku-scalr 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.1.0"
10
+ s.version = "0.2.0"
11
11
 
12
12
  s.authors = ["Black Square Media"]
13
13
  s.email = "info@blacksquaremedia.com"
@@ -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, :min_frequency, :last_scaled_at
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
- 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
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(2) }
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 "scale!" do
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
- 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
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
- 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
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
- 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
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
- 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
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 update dynos" do
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 not scale down if min number of dynos is reached" do
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 "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
86
+ context "up" do
85
87
 
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
88
+ before { subject.metric.stub by: 1 }
90
89
 
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
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 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 })
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.1.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-14 00:00:00.000000000 Z
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