heroku-autoscale 0.1

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