heroku-autoscale 0.1

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,59 @@
1
+ require "rake"
2
+ require "rspec"
3
+ require "rspec/core/rake_task"
4
+
5
+ $:.unshift File.expand_path("../lib", __FILE__)
6
+ require "heroku/autoscale"
7
+
8
+ task :default => :spec
9
+
10
+ desc "Run all specs"
11
+ Rspec::Core::RakeTask.new(:spec) do |t|
12
+ t.pattern = 'spec/**/*_spec.rb'
13
+ end
14
+
15
+ desc "Generate RCov code coverage report"
16
+ task :rcov => "rcov:build" do
17
+ %x{ open coverage/index.html }
18
+ end
19
+
20
+ Rspec::Core::RakeTask.new("rcov:build") do |t|
21
+ t.pattern = 'spec/**/*_spec.rb'
22
+ t.rcov = true
23
+ t.rcov_opts = [ "--exclude", Gem.default_dir , "--exclude", "spec" ]
24
+ end
25
+
26
+ ######################################################
27
+
28
+ begin
29
+ require 'jeweler'
30
+ Jeweler::Tasks.new do |s|
31
+ s.name = "heroku-autoscale"
32
+ s.version = Heroku::Autoscale::VERSION
33
+
34
+ s.summary = "Autoscale your Heroku dynos"
35
+ s.description = s.summary
36
+ s.author = "David Dollar"
37
+ s.email = "ddollar@gmail.com"
38
+ s.homepage = "http://github.com/ddollar/heroku-autoscale"
39
+
40
+ s.platform = Gem::Platform::RUBY
41
+ s.has_rdoc = false
42
+
43
+ s.files = %w(Rakefile README.md) + Dir["{bin,export,lib,spec}/**/*"]
44
+ s.require_path = "lib"
45
+
46
+ s.add_development_dependency 'rack-test', '~> 0.5.4'
47
+ s.add_development_dependency 'rake', '~> 0.8.7'
48
+ s.add_development_dependency 'rcov', '~> 0.9.8'
49
+ s.add_development_dependency 'rr', '~> 0.10.11'
50
+ s.add_development_dependency 'rspec', '~> 2.0.0'
51
+
52
+ s.add_dependency 'eventmachine'
53
+ s.add_dependency 'heroku', '~> 1.9'
54
+ s.add_dependency 'rack', '~> 1.0'
55
+ end
56
+ Jeweler::GemcutterTasks.new
57
+ rescue LoadError
58
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
59
+ end
@@ -0,0 +1,82 @@
1
+ require "eventmachine"
2
+ require "heroku"
3
+ require "rack"
4
+
5
+ module Heroku
6
+ class Autoscale
7
+
8
+ VERSION = "0.1"
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
+ if options[:defer]
21
+ EventMachine.defer { autoscale(env) }
22
+ else
23
+ autoscale(env)
24
+ end
25
+
26
+ app.call(env)
27
+ end
28
+
29
+ private ######################################################################
30
+
31
+ def autoscale(env)
32
+ original_dynos = dynos = current_dynos
33
+ wait = queue_wait(env)
34
+
35
+ dynos -= 1 if wait <= options[:queue_wait_low]
36
+ dynos += 1 if wait >= options[:queue_wait_high]
37
+
38
+ dynos = options[:min_dynos] if dynos < options[:min_dynos]
39
+ dynos = options[:max_dynos] if dynos > options[:max_dynos]
40
+ dynos = 1 if dynos < 1
41
+
42
+ set_dynos(dynos) if dynos != original_dynos
43
+ end
44
+
45
+ def check_options!
46
+ errors = []
47
+ errors << "Must supply :username to Heroku::Autoscale" unless options[:username]
48
+ errors << "Must supply :password to Heroku::Autoscale" unless options[:password]
49
+ errors << "Must supply :app_name to Heroku::Autoscale" unless options[:app_name]
50
+ raise errors.join(" / ") unless errors.empty?
51
+ end
52
+
53
+ def current_dynos
54
+ heroku.info(options[:app_name])[:dynos].to_i
55
+ end
56
+
57
+ def default_options
58
+ {
59
+ :min_dynos => 1,
60
+ :max_dynos => 1,
61
+ :queue_wait_high => 5000, # milliseconds
62
+ :queue_wait_low => 0, # milliseconds
63
+ :min_frequency => 10 # seconds
64
+ }
65
+ end
66
+
67
+ def heroku
68
+ @heroku ||= Heroku::Client.new(options[:username], options[:password])
69
+ end
70
+
71
+ def queue_wait(env)
72
+ env["HTTP_X_HEROKU_QUEUE_WAIT_TIME"]
73
+ end
74
+
75
+ def set_dynos(count)
76
+ return if (Time.now - last_scaled) < options[:min_frequency]
77
+ heroku.set_dynos(options[:app_name], count)
78
+ @last_scaled = Time.now
79
+ end
80
+
81
+ end
82
+ 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").times(any_times) { { :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,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroku-autoscale
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - David Dollar
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-09 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rack-test
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ - 5
32
+ - 4
33
+ version: 0.5.4
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 49
45
+ segments:
46
+ - 0
47
+ - 8
48
+ - 7
49
+ version: 0.8.7
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rcov
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 43
61
+ segments:
62
+ - 0
63
+ - 9
64
+ - 8
65
+ version: 0.9.8
66
+ type: :development
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: rr
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 33
77
+ segments:
78
+ - 0
79
+ - 10
80
+ - 11
81
+ version: 0.10.11
82
+ type: :development
83
+ version_requirements: *id004
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ prerelease: false
87
+ requirement: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ~>
91
+ - !ruby/object:Gem::Version
92
+ hash: 15
93
+ segments:
94
+ - 2
95
+ - 0
96
+ - 0
97
+ version: 2.0.0
98
+ type: :development
99
+ version_requirements: *id005
100
+ - !ruby/object:Gem::Dependency
101
+ name: eventmachine
102
+ prerelease: false
103
+ requirement: &id006 !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 3
109
+ segments:
110
+ - 0
111
+ version: "0"
112
+ type: :runtime
113
+ version_requirements: *id006
114
+ - !ruby/object:Gem::Dependency
115
+ name: heroku
116
+ prerelease: false
117
+ requirement: &id007 !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ~>
121
+ - !ruby/object:Gem::Version
122
+ hash: 29
123
+ segments:
124
+ - 1
125
+ - 9
126
+ version: "1.9"
127
+ type: :runtime
128
+ version_requirements: *id007
129
+ - !ruby/object:Gem::Dependency
130
+ name: rack
131
+ prerelease: false
132
+ requirement: &id008 !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ~>
136
+ - !ruby/object:Gem::Version
137
+ hash: 15
138
+ segments:
139
+ - 1
140
+ - 0
141
+ version: "1.0"
142
+ type: :runtime
143
+ version_requirements: *id008
144
+ description: Autoscale your Heroku dynos
145
+ email: ddollar@gmail.com
146
+ executables: []
147
+
148
+ extensions: []
149
+
150
+ extra_rdoc_files:
151
+ - README.md
152
+ files:
153
+ - README.md
154
+ - Rakefile
155
+ - lib/heroku/autoscale.rb
156
+ - spec/heroku/autoscale_spec.rb
157
+ - spec/spec_helper.rb
158
+ has_rdoc: false
159
+ homepage: http://github.com/ddollar/heroku-autoscale
160
+ licenses: []
161
+
162
+ post_install_message:
163
+ rdoc_options:
164
+ - --charset=UTF-8
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ hash: 3
173
+ segments:
174
+ - 0
175
+ version: "0"
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ hash: 3
182
+ segments:
183
+ - 0
184
+ version: "0"
185
+ requirements: []
186
+
187
+ rubyforge_project:
188
+ rubygems_version: 1.3.7
189
+ signing_key:
190
+ specification_version: 3
191
+ summary: Autoscale your Heroku dynos
192
+ test_files:
193
+ - spec/heroku/autoscale_spec.rb
194
+ - spec/spec_helper.rb