n8-heroku-autoscale 0.2.2
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 +40 -0
- data/Rakefile +59 -0
- data/lib/heroku/autoscale.rb +85 -0
- data/spec/heroku/autoscale_spec.rb +97 -0
- data/spec/spec_helper.rb +10 -0
- metadata +141 -0
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Heroku::Autoscale
|
2
|
+
|
3
|
+
This is a fork of @ddollar's original gem. It's a bit quick and dirty right now.
|
4
|
+
|
5
|
+
The main change I wanted to make was to have the scaling action this performs to be explicit. So instead of having code invoked on every single web request that this gem intercepts (it's a Rack app) which doesn't work for anyone that has multiple dynos going in production, you now have to explicitly ask your app to auto scale using a GET request to the app.
|
6
|
+
|
7
|
+
With that in place, you can issue that GET request from cron or clockwork or some alternative so that you don't have to worry about your autoscale commands running concurently.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
# Gemfile
|
12
|
+
gem 'heroku-autoscale', :git => "git@github.com:n8/heroku-autoscale.git"
|
13
|
+
|
14
|
+
## Usage (Rails 2.x)
|
15
|
+
|
16
|
+
# config/environment.rb
|
17
|
+
use Heroku::Autoscale,
|
18
|
+
:username => ENV["HEROKU_USERNAME"],
|
19
|
+
:password => ENV["HEROKU_PASSWORD"],
|
20
|
+
:app_name => ENV["HEROKU_APP_NAME"],
|
21
|
+
:autoscale_key => ENV["HEROKU_SCALE_KEY"],
|
22
|
+
:min_dynos => 5,
|
23
|
+
:max_dynos => 30,
|
24
|
+
:queue_wait_low => 100, # milliseconds
|
25
|
+
:queue_wait_high => 1000, # milliseconds
|
26
|
+
:min_frequency => 10 # seconds
|
27
|
+
|
28
|
+
## Usage (Rails 3 / Rack)
|
29
|
+
|
30
|
+
# config.ru
|
31
|
+
use Heroku::Autoscale,
|
32
|
+
:username => ENV["HEROKU_USERNAME"],
|
33
|
+
:password => ENV["HEROKU_PASSWORD"],
|
34
|
+
:app_name => ENV["HEROKU_APP_NAME"],
|
35
|
+
:autoscale_key => ENV["HEROKU_SCALE_KEY"],
|
36
|
+
:min_dynos => 5,
|
37
|
+
:max_dynos => 30,
|
38
|
+
:queue_wait_low => 100, # milliseconds
|
39
|
+
:queue_wait_high => 1000, # milliseconds
|
40
|
+
: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,85 @@
|
|
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
|
+
if env["PATH_INFO"] == "/autoscale/#{options[:autoscale_key]}"
|
21
|
+
autoscale env
|
22
|
+
[200, {'Content-Type' => 'text/plain'}, ["Current wait time: #{env["HTTP_X_HEROKU_QUEUE_WAIT_TIME"]}"]]
|
23
|
+
else
|
24
|
+
app.call(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private ######################################################################
|
29
|
+
|
30
|
+
def autoscale(env)
|
31
|
+
# dont do anything if we scaled too frequently ago
|
32
|
+
return if (Time.now - last_scaled) < options[:min_frequency]
|
33
|
+
|
34
|
+
original_dynos = dynos = current_dynos
|
35
|
+
wait = queue_wait(env)
|
36
|
+
|
37
|
+
dynos -= 1 if wait <= options[:queue_wait_low]
|
38
|
+
dynos += 1 if wait >= options[:queue_wait_high]
|
39
|
+
|
40
|
+
dynos = options[:min_dynos] if dynos < options[:min_dynos]
|
41
|
+
dynos = options[:max_dynos] if dynos > options[:max_dynos]
|
42
|
+
dynos = 1 if dynos < 1
|
43
|
+
|
44
|
+
set_dynos(dynos) if dynos != original_dynos
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_options!
|
48
|
+
errors = []
|
49
|
+
errors << "Must supply :username to Heroku::Autoscale" unless options[:username]
|
50
|
+
errors << "Must supply :password to Heroku::Autoscale" unless options[:password]
|
51
|
+
errors << "Must supply :app_name to Heroku::Autoscale" unless options[:app_name]
|
52
|
+
raise errors.join(" / ") unless errors.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def current_dynos
|
56
|
+
# heroku.info(options[:app_name])[:dynos].to_i
|
57
|
+
heroku.ps(options[:app_name]).select{|jobby| jobby['process'].index("web.") != nil}.size
|
58
|
+
end
|
59
|
+
|
60
|
+
def default_options
|
61
|
+
{
|
62
|
+
:defer => true,
|
63
|
+
:min_dynos => 1,
|
64
|
+
:max_dynos => 1,
|
65
|
+
:queue_wait_high => 5000, # milliseconds
|
66
|
+
:queue_wait_low => 0, # milliseconds
|
67
|
+
:min_frequency => 10 # seconds
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def heroku
|
72
|
+
@heroku ||= Heroku::Client.new(options[:username], options[:password])
|
73
|
+
end
|
74
|
+
|
75
|
+
def queue_wait(env)
|
76
|
+
env["HTTP_X_HEROKU_QUEUE_WAIT_TIME"].to_i
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_dynos(count)
|
80
|
+
heroku.ps_scale(options[:app_name], :type => 'web', :qty => count)
|
81
|
+
@last_scaled = Time.now
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
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
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: n8-heroku-autoscale
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Dollar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2010-07-09 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack-test
|
16
|
+
requirement: &70193737265220 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.5.4
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70193737265220
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70193737264740 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.7
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70193737264740
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rcov
|
38
|
+
requirement: &70193737264260 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.9.8
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70193737264260
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rr
|
49
|
+
requirement: &70193737263780 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.10.11
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70193737263780
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &70193737263300 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.0.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70193737263300
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: eventmachine
|
71
|
+
requirement: &70193737262820 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70193737262820
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: heroku
|
82
|
+
requirement: &70193737262280 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 2.9.0
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70193737262280
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rack
|
93
|
+
requirement: &70193737261740 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ~>
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '1.0'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70193737261740
|
102
|
+
description: Autoscale your Heroku dynos
|
103
|
+
email: ddollar@gmail.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files:
|
107
|
+
- README.md
|
108
|
+
files:
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/heroku/autoscale.rb
|
112
|
+
- spec/heroku/autoscale_spec.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
homepage: http://github.com/ddollar/heroku-autoscale
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options:
|
118
|
+
- --charset=UTF-8
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 1.8.10
|
136
|
+
signing_key:
|
137
|
+
specification_version: 3
|
138
|
+
summary: Autoscale your Heroku dynos
|
139
|
+
test_files:
|
140
|
+
- spec/heroku/autoscale_spec.rb
|
141
|
+
- spec/spec_helper.rb
|