Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ddollar committed Jul 10, 2010
0 parents commit ba1be1a
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage/
32 changes: 32 additions & 0 deletions README.md
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
60 changes: 60 additions & 0 deletions Rakefile
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
57 changes: 57 additions & 0 deletions heroku-autoscale.gemspec
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

82 changes: 82 additions & 0 deletions lib/heroku/autoscale.rb
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
97 changes: 97 additions & 0 deletions spec/heroku/autoscale_spec.rb
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
10 changes: 10 additions & 0 deletions spec/spec_helper.rb
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

0 comments on commit ba1be1a

Please sign in to comment.