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 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