n8-heroku-autoscale 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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,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