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.
@@ -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