forked from ddollar/heroku-autoscale
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ba1be1a
Showing
7 changed files
with
339 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
coverage/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Heroku::Autoscale | ||
|
||
## Installation | ||
|
||
# Gemfile | ||
gem 'heroku-autoscale' | ||
|
||
## Usage (Rails 2.x) | ||
|
||
# config/environment.rb | ||
config.middleware.use Heroku::Autoscale, | ||
:username => ENV["HEROKU_USERNAME"], | ||
:password => ENV["HEROKU_PASSWORD"], | ||
:app_name => ENV["HEROKU_APP_NAME"], | ||
:min_dynos => 2, | ||
:max_dynos => 5, | ||
:queue_wait_low => 100, # milliseconds | ||
:queue_wait_high => 5000, # milliseconds | ||
:min_frequency => 10 # seconds | ||
|
||
## Usage (Rails 3 / Rack) | ||
|
||
# config.ru | ||
use Heroku::Autoscale, | ||
:username => ENV["HEROKU_USERNAME"], | ||
:password => ENV["HEROKU_PASSWORD"], | ||
:app_name => ENV["HEROKU_APP_NAME"], | ||
:min_dynos => 2, | ||
:max_dynos => 5, | ||
:queue_wait_low => 100, # milliseconds | ||
:queue_wait_high => 5000, # milliseconds | ||
:min_frequency => 10 # seconds |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
require "rake" | ||
require "rspec" | ||
require "rspec/core/rake_task" | ||
|
||
$:.unshift File.expand_path("../lib", __FILE__) | ||
require "heroku/autoscale" | ||
|
||
task :default => :spec | ||
task :release => :man | ||
|
||
desc "Run all specs" | ||
Rspec::Core::RakeTask.new(:spec) do |t| | ||
t.pattern = 'spec/**/*_spec.rb' | ||
end | ||
|
||
desc "Generate RCov code coverage report" | ||
task :rcov => "rcov:build" do | ||
%x{ open coverage/index.html } | ||
end | ||
|
||
Rspec::Core::RakeTask.new("rcov:build") do |t| | ||
t.pattern = 'spec/**/*_spec.rb' | ||
t.rcov = true | ||
t.rcov_opts = [ "--exclude", Gem.default_dir , "--exclude", "spec" ] | ||
end | ||
|
||
###################################################### | ||
|
||
begin | ||
require 'jeweler' | ||
Jeweler::Tasks.new do |s| | ||
s.name = "heroku-autoscale" | ||
s.version = Heroku::Autoscale::VERSION | ||
|
||
s.summary = "Autoscale your Heroku dynos" | ||
s.description = s.summary | ||
s.author = "David Dollar" | ||
s.email = "ddollar@gmail.com" | ||
s.homepage = "http://github.com/ddollar/heroku-autoscale" | ||
|
||
s.platform = Gem::Platform::RUBY | ||
s.has_rdoc = false | ||
|
||
s.files = %w(Rakefile README.md) + Dir["{bin,export,lib,spec}/**/*"] | ||
s.require_path = "lib" | ||
|
||
s.add_development_dependency 'rack-test', '~> 0.5.4' | ||
s.add_development_dependency 'rake', '~> 0.8.7' | ||
s.add_development_dependency 'rcov', '~> 0.9.8' | ||
s.add_development_dependency 'rr', '~> 0.10.11' | ||
s.add_development_dependency 'rspec', '~> 2.0.0' | ||
|
||
s.add_dependency 'eventmachine' | ||
s.add_dependency 'heroku', '~> 1.9' | ||
s.add_dependency 'rack', '~> 1.0' | ||
end | ||
Jeweler::GemcutterTasks.new | ||
rescue LoadError | ||
puts "Jeweler not available. Install it with: sudo gem install jeweler" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Generated by jeweler | ||
# DO NOT EDIT THIS FILE DIRECTLY | ||
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command | ||
# -*- encoding: utf-8 -*- | ||
|
||
Gem::Specification.new do |s| | ||
s.name = %q{heroku-autoscale} | ||
s.version = "0.1" | ||
|
||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= | ||
s.authors = ["David Dollar"] | ||
s.date = %q{2010-07-09} | ||
s.description = %q{Autoscale your Heroku dynos} | ||
s.email = %q{ddollar@gmail.com} | ||
s.files = [ | ||
"Rakefile", | ||
"lib/heroku/autoscale.rb" | ||
] | ||
s.has_rdoc = false | ||
s.homepage = %q{http://github.com/ddollar/heroku-autoscale} | ||
s.rdoc_options = ["--charset=UTF-8"] | ||
s.require_paths = ["lib"] | ||
s.rubygems_version = %q{1.3.7} | ||
s.summary = %q{Autoscale your Heroku dynos} | ||
|
||
if s.respond_to? :specification_version then | ||
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION | ||
s.specification_version = 3 | ||
|
||
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then | ||
s.add_development_dependency(%q<rake>, ["~> 0.8.7"]) | ||
s.add_development_dependency(%q<rcov>, ["~> 0.9.8"]) | ||
s.add_development_dependency(%q<rr>, ["~> 0.10.11"]) | ||
s.add_development_dependency(%q<rspec>, ["~> 2.0.0"]) | ||
s.add_runtime_dependency(%q<eventmachine>, [">= 0"]) | ||
s.add_runtime_dependency(%q<heroku>, ["~> 1.9"]) | ||
s.add_runtime_dependency(%q<rack>, ["~> 1.0"]) | ||
else | ||
s.add_dependency(%q<rake>, ["~> 0.8.7"]) | ||
s.add_dependency(%q<rcov>, ["~> 0.9.8"]) | ||
s.add_dependency(%q<rr>, ["~> 0.10.11"]) | ||
s.add_dependency(%q<rspec>, ["~> 2.0.0"]) | ||
s.add_dependency(%q<eventmachine>, [">= 0"]) | ||
s.add_dependency(%q<heroku>, ["~> 1.9"]) | ||
s.add_dependency(%q<rack>, ["~> 1.0"]) | ||
end | ||
else | ||
s.add_dependency(%q<rake>, ["~> 0.8.7"]) | ||
s.add_dependency(%q<rcov>, ["~> 0.9.8"]) | ||
s.add_dependency(%q<rr>, ["~> 0.10.11"]) | ||
s.add_dependency(%q<rspec>, ["~> 2.0.0"]) | ||
s.add_dependency(%q<eventmachine>, [">= 0"]) | ||
s.add_dependency(%q<heroku>, ["~> 1.9"]) | ||
s.add_dependency(%q<rack>, ["~> 1.0"]) | ||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
require "eventmachine" | ||
require "heroku" | ||
require "rack" | ||
|
||
module Heroku | ||
class Autoscale | ||
|
||
VERSION = "0.1" | ||
|
||
attr_reader :app, :options, :last_scaled | ||
|
||
def initialize(app, options={}) | ||
@app = app | ||
@options = default_options.merge(options) | ||
@last_scaled = Time.now - 60 | ||
check_options! | ||
end | ||
|
||
def call(env) | ||
if options[:defer] | ||
EventMachine.defer { autoscale(env) } | ||
else | ||
autoscale(env) | ||
end | ||
|
||
app.call(env) | ||
end | ||
|
||
private ###################################################################### | ||
|
||
def autoscale(env) | ||
original_dynos = dynos = current_dynos | ||
wait = queue_wait(env) | ||
|
||
dynos -= 1 if wait <= options[:queue_wait_low] | ||
dynos += 1 if wait >= options[:queue_wait_high] | ||
|
||
dynos = options[:min_dynos] if dynos < options[:min_dynos] | ||
dynos = options[:max_dynos] if dynos > options[:max_dynos] | ||
dynos = 1 if dynos < 1 | ||
|
||
set_dynos(dynos) if dynos != original_dynos | ||
end | ||
|
||
def check_options! | ||
errors = [] | ||
errors << "Must supply :username to Heroku::Autoscale" unless options[:username] | ||
errors << "Must supply :password to Heroku::Autoscale" unless options[:password] | ||
errors << "Must supply :app_name to Heroku::Autoscale" unless options[:app_name] | ||
raise errors.join(" / ") unless errors.empty? | ||
end | ||
|
||
def current_dynos | ||
heroku.info(options[:app_name])[:dynos].to_i | ||
end | ||
|
||
def default_options | ||
{ | ||
:min_dynos => 1, | ||
:max_dynos => 1, | ||
:queue_wait_high => 5000, # milliseconds | ||
:queue_wait_low => 0, # milliseconds | ||
:min_frequency => 10 # seconds | ||
} | ||
end | ||
|
||
def heroku | ||
@heroku ||= Heroku::Client.new(options[:username], options[:password]) | ||
end | ||
|
||
def queue_wait(env) | ||
env["HTTP_X_HEROKU_QUEUE_WAIT_TIME"] | ||
end | ||
|
||
def set_dynos(count) | ||
return if (Time.now - last_scaled) < options[:min_frequency] | ||
heroku.set_dynos(options[:app_name], count) | ||
@last_scaled = Time.now | ||
end | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
require "spec_helper" | ||
require "heroku/autoscale" | ||
|
||
describe Heroku::Autoscale do | ||
|
||
include Rack::Test::Methods | ||
|
||
def noop | ||
lambda {} | ||
end | ||
|
||
describe "option validation" do | ||
it "requires username" do | ||
lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :username/) | ||
end | ||
|
||
it "requires password" do | ||
lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :password/) | ||
end | ||
|
||
it "requires app_name" do | ||
lambda { Heroku::Autoscale.new(noop) }.should raise_error(/Must supply :app_name/) | ||
end | ||
end | ||
|
||
describe "with valid options" do | ||
let(:app) do | ||
Heroku::Autoscale.new noop, | ||
:defer => false, | ||
:username => "test_username", | ||
:password => "test_password", | ||
:app_name => "test_app_name", | ||
:min_dynos => 1, | ||
:max_dynos => 10, | ||
:queue_wait_low => 10, | ||
:queue_wait_high => 100, | ||
:min_frequency => 10 | ||
end | ||
|
||
it "scales up" do | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name") { { :dynos => 1 } } | ||
heroku.set_dynos("test_app_name", 2) | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 101 }) | ||
end | ||
|
||
it "scales down" do | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name") { { :dynos => 3 } } | ||
heroku.set_dynos("test_app_name", 2) | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 }) | ||
end | ||
|
||
it "wont go below one dyno" do | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name") { { :dynos => 1 } } | ||
heroku.set_dynos.times(0) | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 }) | ||
end | ||
|
||
it "respects max dynos" do | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name") { { :dynos => 10 } } | ||
heroku.set_dynos.times(0) | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 101 }) | ||
end | ||
|
||
it "respects min dynos" do | ||
app.options[:min_dynos] = 2 | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name") { { :dynos => 2 } } | ||
heroku.set_dynos.times(0) | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 }) | ||
end | ||
|
||
it "doesnt flap" do | ||
heroku = mock(Heroku::Client) | ||
heroku.info("test_app_name").times(any_times) { { :dynos => 5 } } | ||
heroku.set_dynos.with_any_args.once | ||
|
||
mock(app).heroku.times(any_times) { heroku } | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 }) | ||
app.call({ "HTTP_X_HEROKU_QUEUE_WAIT_TIME" => 9 }) | ||
end | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
require "rubygems" | ||
require "rack/test" | ||
require "rspec" | ||
|
||
$:.unshift "lib" | ||
|
||
Rspec.configure do |config| | ||
config.color_enabled = true | ||
config.mock_with :rr | ||
end |