mbeedle-heroku-autoscale 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Heroku::Autoscale
2
+
3
+ ## Installation
4
+
5
+ # Gemfile
6
+ gem 'heroku-autoscale'
7
+
8
+ ## Usage (Rails 2.x)
9
+
10
+ # config/environment.rb
11
+ config.middleware.use Heroku::Autoscale,
12
+ :username => ENV["HEROKU_USERNAME"],
13
+ :password => ENV["HEROKU_PASSWORD"],
14
+ :app_name => ENV["HEROKU_APP_NAME"],
15
+ :min_dynos => 2,
16
+ :max_dynos => 5,
17
+ :queue_wait_low => 100, # milliseconds
18
+ :queue_wait_high => 5000, # milliseconds
19
+ :min_frequency => 10 # seconds
20
+
21
+ ## Usage (Rails 3 / Rack)
22
+
23
+ # config.ru
24
+ use Heroku::Autoscale,
25
+ :username => ENV["HEROKU_USERNAME"],
26
+ :password => ENV["HEROKU_PASSWORD"],
27
+ :app_name => ENV["HEROKU_APP_NAME"],
28
+ :min_dynos => 2,
29
+ :max_dynos => 5,
30
+ :queue_wait_low => 100, # milliseconds
31
+ :queue_wait_high => 5000, # milliseconds
32
+ :min_frequency => 10 # seconds
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require "rake"
2
+ $:.unshift File.expand_path("../lib", __FILE__)
3
+ require "heroku/autoscale"
4
+
5
+ ######################################################
6
+
7
+ begin
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |s|
10
+ s.name = "mbeedle-heroku-autoscale"
11
+ s.version = Heroku::Autoscale::VERSION
12
+
13
+ s.summary = "Autoscale your Heroku dynos"
14
+ s.description = s.summary
15
+ s.author = "David Dollar"
16
+ s.email = "ddollar@gmail.com"
17
+ s.homepage = "http://github.com/ddollar/heroku-autoscale"
18
+
19
+ s.platform = Gem::Platform::RUBY
20
+ s.has_rdoc = false
21
+
22
+ s.files = %w(Rakefile README.md) + Dir["{bin,export,lib,spec}/**/*"]
23
+ s.require_path = "lib"
24
+
25
+ s.add_development_dependency 'rack-test', '~> 0.5.4'
26
+ s.add_development_dependency 'rake', '~> 0.8.7'
27
+ s.add_development_dependency 'rcov', '~> 0.9.8'
28
+ s.add_development_dependency 'rr', '~> 0.10.11'
29
+ s.add_development_dependency 'rspec', '~> 2.0.0'
30
+
31
+ s.add_dependency 'eventmachine'
32
+ s.add_dependency 'heroku', '~> 1.9'
33
+ s.add_dependency 'rack', '~> 1.0'
34
+ end
35
+ Jeweler::GemcutterTasks.new
36
+ rescue LoadError
37
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
38
+ end
@@ -0,0 +1,88 @@
1
+ require "eventmachine"
2
+ require "heroku"
3
+ require "rack"
4
+
5
+ module Heroku
6
+ class Autoscale
7
+
8
+ VERSION = "0.2.2"
9
+
10
+ attr_reader :app, :options, :last_scaled
11
+
12
+ def initialize(app, options={})
13
+ @app = app
14
+ @options = default_options.merge(options)
15
+ @last_scaled = Time.now - 60
16
+ check_options!
17
+ end
18
+
19
+ def call(env)
20
+ begin
21
+ if options[:defer]
22
+ EventMachine.defer { autoscale(env) }
23
+ else
24
+ autoscale(env)
25
+ end
26
+
27
+ app.call(env)
28
+ rescue RestClient::BadGateway => e
29
+ end
30
+ end
31
+
32
+ private ######################################################################
33
+
34
+ def autoscale(env)
35
+ # dont do anything if we scaled too frequently ago
36
+ return if (Time.now - last_scaled) < options[:min_frequency]
37
+
38
+ original_dynos = dynos = current_dynos
39
+ wait = queue_wait(env)
40
+
41
+ dynos -= 1 if wait <= options[:queue_wait_low]
42
+ dynos += 1 if wait >= options[:queue_wait_high]
43
+
44
+ dynos = options[:min_dynos] if dynos < options[:min_dynos]
45
+ dynos = options[:max_dynos] if dynos > options[:max_dynos]
46
+ dynos = 1 if dynos < 1
47
+
48
+ set_dynos(dynos) if dynos != original_dynos
49
+ end
50
+
51
+ def check_options!
52
+ errors = []
53
+ errors << "Must supply :username to Heroku::Autoscale" unless options[:username]
54
+ errors << "Must supply :password to Heroku::Autoscale" unless options[:password]
55
+ errors << "Must supply :app_name to Heroku::Autoscale" unless options[:app_name]
56
+ raise errors.join(" / ") unless errors.empty?
57
+ end
58
+
59
+ def current_dynos
60
+ heroku.info(options[:app_name])[:dynos].to_i
61
+ end
62
+
63
+ def default_options
64
+ {
65
+ :defer => true,
66
+ :min_dynos => 1,
67
+ :max_dynos => 1,
68
+ :queue_wait_high => 5000, # milliseconds
69
+ :queue_wait_low => 0, # milliseconds
70
+ :min_frequency => 10 # seconds
71
+ }
72
+ end
73
+
74
+ def heroku
75
+ @heroku ||= Heroku::Client.new(options[:username], options[:password])
76
+ end
77
+
78
+ def queue_wait(env)
79
+ env["HTTP_X_HEROKU_QUEUE_WAIT_TIME"].to_i
80
+ end
81
+
82
+ def set_dynos(count)
83
+ heroku.set_dynos(options[:app_name], count)
84
+ @last_scaled = Time.now
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,97 @@
1
+ require "spec_helper"
2
+ require "heroku/autoscale"
3
+
4
+ describe Heroku::Autoscale do
5
+
6
+ include Rack::Test::Methods
7
+
8
+ def noop
9
+ lambda {}
10
+ end
11
+
12
+ describe "option validation" do
13
+ it "requires username" do
14
+ lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :username/)
15
+ end
16
+
17
+ it "requires password" do
18
+ lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :password/)
19
+ end
20
+
21
+ it "requires app_name" do
22
+ lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :app_name/)
23
+ end
24
+ end
25
+
26
+ describe "with valid options" do
27
+ let(:app) do
28
+ Heroku::Autoscale.new noop,
29
+ :defer => false,
30
+ :username => "test_username",
31
+ :password => "test_password",
32
+ :app_name => "test_app_name",
33
+ :min_dynos => 1,
34
+ :max_dynos => 10,
35
+ :queue_wait_low => 10,
36
+ :queue_wait_high => 100,
37
+ :min_frequency => 10
38
+ end
39
+
40
+ it "scales up" do
41
+ heroku = mock(Heroku::Client)
42
+ heroku.info("test_app_name") { { :dynos => 1 } }
43
+ heroku.set_dynos("test_app_name", 2)
44
+
45
+ mock(app).heroku.times(any_times) { heroku }
46
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 101 })
47
+ end
48
+
49
+ it "scales down" do
50
+ heroku = mock(Heroku::Client)
51
+ heroku.info("test_app_name") { { :dynos => 3 } }
52
+ heroku.set_dynos("test_app_name", 2)
53
+
54
+ mock(app).heroku.times(any_times) { heroku }
55
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 })
56
+ end
57
+
58
+ it "wont go below one dyno" do
59
+ heroku = mock(Heroku::Client)
60
+ heroku.info("test_app_name") { { :dynos => 1 } }
61
+ heroku.set_dynos.times(0)
62
+
63
+ mock(app).heroku.times(any_times) { heroku }
64
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 })
65
+ end
66
+
67
+ it "respects max dynos" do
68
+ heroku = mock(Heroku::Client)
69
+ heroku.info("test_app_name") { { :dynos => 10 } }
70
+ heroku.set_dynos.times(0)
71
+
72
+ mock(app).heroku.times(any_times) { heroku }
73
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 101 })
74
+ end
75
+
76
+ it "respects min dynos" do
77
+ app.options[:min_dynos] = 2
78
+ heroku = mock(Heroku::Client)
79
+ heroku.info("test_app_name") { { :dynos => 2 } }
80
+ heroku.set_dynos.times(0)
81
+
82
+ mock(app).heroku.times(any_times) { heroku }
83
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 })
84
+ end
85
+
86
+ it "doesnt flap" do
87
+ heroku = mock(Heroku::Client)
88
+ heroku.info("test_app_name").once { { :dynos => 5 } }
89
+ heroku.set_dynos.with_any_args.once
90
+
91
+ mock(app).heroku.times(any_times) { heroku }
92
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 })
93
+ app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 })
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,10 @@
1
+ require "rubygems"
2
+ require "rack/test"
3
+ require "rspec"
4
+
5
+ $:.unshift "lib"
6
+
7
+ Rspec.configure do |config|
8
+ config.color_enabled = true
9
+ config.mock_with :rr
10
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mbeedle-heroku-autoscale
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 2
10
+ version: 0.2.2
11
+ platform: ruby
12
+ authors:
13
+ - David Dollar
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-24 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rack-test
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ - 5
33
+ - 4
34
+ version: 0.5.4
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rake
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 49
46
+ segments:
47
+ - 0
48
+ - 8
49
+ - 7
50
+ version: 0.8.7
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: rcov
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 43
62
+ segments:
63
+ - 0
64
+ - 9
65
+ - 8
66
+ version: 0.9.8
67
+ type: :development
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: rr
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ hash: 33
78
+ segments:
79
+ - 0
80
+ - 10
81
+ - 11
82
+ version: 0.10.11
83
+ type: :development
84
+ version_requirements: *id004
85
+ - !ruby/object:Gem::Dependency
86
+ name: rspec
87
+ prerelease: false
88
+ requirement: &id005 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ hash: 15
94
+ segments:
95
+ - 2
96
+ - 0
97
+ - 0
98
+ version: 2.0.0
99
+ type: :development
100
+ version_requirements: *id005
101
+ - !ruby/object:Gem::Dependency
102
+ name: eventmachine
103
+ prerelease: false
104
+ requirement: &id006 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ type: :runtime
114
+ version_requirements: *id006
115
+ - !ruby/object:Gem::Dependency
116
+ name: heroku
117
+ prerelease: false
118
+ requirement: &id007 !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ hash: 29
124
+ segments:
125
+ - 1
126
+ - 9
127
+ version: "1.9"
128
+ type: :runtime
129
+ version_requirements: *id007
130
+ - !ruby/object:Gem::Dependency
131
+ name: rack
132
+ prerelease: false
133
+ requirement: &id008 !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ hash: 15
139
+ segments:
140
+ - 1
141
+ - 0
142
+ version: "1.0"
143
+ type: :runtime
144
+ version_requirements: *id008
145
+ description: Autoscale your Heroku dynos
146
+ email: ddollar@gmail.com
147
+ executables: []
148
+
149
+ extensions: []
150
+
151
+ extra_rdoc_files:
152
+ - README.md
153
+ files:
154
+ - README.md
155
+ - Rakefile
156
+ - lib/heroku/autoscale.rb
157
+ - spec/heroku/autoscale_spec.rb
158
+ - spec/spec_helper.rb
159
+ has_rdoc: true
160
+ homepage: http://github.com/ddollar/heroku-autoscale
161
+ licenses: []
162
+
163
+ post_install_message:
164
+ rdoc_options:
165
+ - --charset=UTF-8
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ hash: 3
174
+ segments:
175
+ - 0
176
+ version: "0"
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ hash: 3
183
+ segments:
184
+ - 0
185
+ version: "0"
186
+ requirements: []
187
+
188
+ rubyforge_project:
189
+ rubygems_version: 1.3.7
190
+ signing_key:
191
+ specification_version: 3
192
+ summary: Autoscale your Heroku dynos
193
+ test_files:
194
+ - spec/spec_helper.rb
195
+ - spec/heroku/autoscale_spec.rb