dyno_scaler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format documentation
3
+ --debug
4
+ --drb
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3-p0@dyno_scaler --create
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dyno_scaler.gemspec
4
+ gemspec
5
+
6
+ group(:development) do
7
+ platforms :mri_19 do
8
+ gem 'debugger'
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem 'ruby-debug'
13
+ gem 'jruby-openssl'
14
+ end
15
+ end
16
+
17
+ group(:test) do
18
+ gem 'simplecov', require: false
19
+ gem 'rails'
20
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,158 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dyno_scaler (0.1.0)
5
+ activesupport
6
+ heroku-api
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionmailer (3.2.9)
12
+ actionpack (= 3.2.9)
13
+ mail (~> 2.4.4)
14
+ actionpack (3.2.9)
15
+ activemodel (= 3.2.9)
16
+ activesupport (= 3.2.9)
17
+ builder (~> 3.0.0)
18
+ erubis (~> 2.7.0)
19
+ journey (~> 1.0.4)
20
+ rack (~> 1.4.0)
21
+ rack-cache (~> 1.2)
22
+ rack-test (~> 0.6.1)
23
+ sprockets (~> 2.2.1)
24
+ activemodel (3.2.9)
25
+ activesupport (= 3.2.9)
26
+ builder (~> 3.0.0)
27
+ activerecord (3.2.9)
28
+ activemodel (= 3.2.9)
29
+ activesupport (= 3.2.9)
30
+ arel (~> 3.0.2)
31
+ tzinfo (~> 0.3.29)
32
+ activeresource (3.2.9)
33
+ activemodel (= 3.2.9)
34
+ activesupport (= 3.2.9)
35
+ activesupport (3.2.9)
36
+ i18n (~> 0.6)
37
+ multi_json (~> 1.0)
38
+ arel (3.0.2)
39
+ bouncy-castle-java (1.5.0146.1)
40
+ builder (3.0.4)
41
+ columnize (0.3.6)
42
+ connection_pool (0.9.3)
43
+ debugger (1.2.2)
44
+ columnize (>= 0.3.1)
45
+ debugger-linecache (~> 1.1.1)
46
+ debugger-ruby_core_source (~> 1.1.5)
47
+ debugger-linecache (1.1.2)
48
+ debugger-ruby_core_source (>= 1.1.1)
49
+ debugger-ruby_core_source (1.1.5)
50
+ diff-lcs (1.1.3)
51
+ erubis (2.7.0)
52
+ excon (0.16.10)
53
+ girl_friday (0.11.1)
54
+ connection_pool (~> 0.9.0)
55
+ rubinius-actor
56
+ heroku-api (0.3.7)
57
+ excon (~> 0.16.10)
58
+ hike (1.2.1)
59
+ i18n (0.6.1)
60
+ journey (1.0.4)
61
+ jruby-openssl (0.8.2)
62
+ bouncy-castle-java (>= 1.5.0146.1)
63
+ json (1.7.5)
64
+ json (1.7.5-java)
65
+ mail (2.4.4)
66
+ i18n (>= 0.4.0)
67
+ mime-types (~> 1.16)
68
+ treetop (~> 1.4.8)
69
+ mime-types (1.19)
70
+ multi_json (1.5.0)
71
+ polyglot (0.3.3)
72
+ rack (1.4.1)
73
+ rack-cache (1.2)
74
+ rack (>= 0.4)
75
+ rack-protection (1.3.2)
76
+ rack
77
+ rack-ssl (1.3.2)
78
+ rack
79
+ rack-test (0.6.2)
80
+ rack (>= 1.0)
81
+ rails (3.2.9)
82
+ actionmailer (= 3.2.9)
83
+ actionpack (= 3.2.9)
84
+ activerecord (= 3.2.9)
85
+ activeresource (= 3.2.9)
86
+ activesupport (= 3.2.9)
87
+ bundler (~> 1.0)
88
+ railties (= 3.2.9)
89
+ railties (3.2.9)
90
+ actionpack (= 3.2.9)
91
+ activesupport (= 3.2.9)
92
+ rack-ssl (~> 1.3.2)
93
+ rake (>= 0.8.7)
94
+ rdoc (~> 3.4)
95
+ thor (>= 0.14.6, < 2.0)
96
+ rake (10.0.3)
97
+ rdoc (3.12)
98
+ json (~> 1.4)
99
+ redis (3.0.2)
100
+ redis-namespace (1.2.1)
101
+ redis (~> 3.0.0)
102
+ resque (1.23.0)
103
+ multi_json (~> 1.0)
104
+ redis-namespace (~> 1.0)
105
+ sinatra (>= 0.9.2)
106
+ vegas (~> 0.1.2)
107
+ rspec (2.12.0)
108
+ rspec-core (~> 2.12.0)
109
+ rspec-expectations (~> 2.12.0)
110
+ rspec-mocks (~> 2.12.0)
111
+ rspec-core (2.12.2)
112
+ rspec-expectations (2.12.1)
113
+ diff-lcs (~> 1.1.3)
114
+ rspec-mocks (2.12.1)
115
+ rubinius-actor (0.0.2)
116
+ rubinius-core-api
117
+ rubinius-core-api (0.0.1)
118
+ rubinius-core-api (0.0.1-java)
119
+ ruby-debug (0.10.4)
120
+ columnize (>= 0.1)
121
+ ruby-debug-base (~> 0.10.4.0)
122
+ ruby-debug-base (0.10.4-java)
123
+ simplecov (0.7.1)
124
+ multi_json (~> 1.0)
125
+ simplecov-html (~> 0.7.1)
126
+ simplecov-html (0.7.1)
127
+ sinatra (1.3.3)
128
+ rack (~> 1.3, >= 1.3.6)
129
+ rack-protection (~> 1.2)
130
+ tilt (~> 1.3, >= 1.3.3)
131
+ sprockets (2.2.2)
132
+ hike (~> 1.2)
133
+ multi_json (~> 1.0)
134
+ rack (~> 1.0)
135
+ tilt (~> 1.1, != 1.3.0)
136
+ thor (0.16.0)
137
+ tilt (1.3.3)
138
+ treetop (1.4.12)
139
+ polyglot
140
+ polyglot (>= 0.3.1)
141
+ tzinfo (0.3.35)
142
+ vegas (0.1.11)
143
+ rack (>= 1.0.0)
144
+
145
+ PLATFORMS
146
+ java
147
+ ruby
148
+
149
+ DEPENDENCIES
150
+ debugger
151
+ dyno_scaler!
152
+ girl_friday
153
+ jruby-openssl
154
+ rails
155
+ resque
156
+ rspec
157
+ ruby-debug
158
+ simplecov
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Vicente Mundim
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # DynoScaler
2
+
3
+ [![alt build status][1]][2]
4
+
5
+ [1]: https://travis-ci.org/dtmconsultoria/dyno_scaler.png?branch=master
6
+ [2]: http://travis-ci.org/dtmconsultoria/dyno_scaler
7
+
8
+ Scale your dyno workers on Heroku as needed, pay only for what you use!
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'dyno_scaler'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install dyno_scaler
23
+
24
+ ## Usage
25
+
26
+ Just include this module in your Resque job and you're good to go:
27
+
28
+ class MyJob
29
+ include DynoScaler::Workers::Resque
30
+
31
+ ...
32
+ end
33
+
34
+ You can access the configuration with (for example):
35
+
36
+ DynoScaler.configuration.max_workers = 3
37
+
38
+ In Rails, you can access it easily in your application.rb:
39
+
40
+ config.dyno_scaler.max_workers = 3
41
+
42
+ If you want to scale up or down your workers manually, you can use the manager
43
+ directly:
44
+
45
+ DynoScaler.manager.scale_up(options)
46
+ DynoScaler.manager.scale_down(options)
47
+
48
+ You must pass an options hash with the number of workers, the number of pending
49
+ jobs, and the number of running jobs, like so:
50
+
51
+ {
52
+ workers: 10,
53
+ working: 3,
54
+ pending: 5
55
+ }
56
+
57
+ `Resque.info` returns a hash with these keys, so you may just pass it instead:
58
+
59
+ DynoScaler.manager.scale_up(Resque.info)
60
+
61
+ You can also use the `DynoScaler::Manager#scale_with` method, passing the `Resque.info`:
62
+
63
+ DynoScaler.manager.scale_with(Resque.info)
64
+
65
+ It will check whether to scale up or down based on the number of workers running,
66
+ pending jobs, and working jobs.
67
+
68
+ ## Heroku Deploy
69
+
70
+ When deploying to heroku you'll want to add these two configuration keys:
71
+
72
+ HEROKU_API_KEY=<YOUR-API-KEY-HERE>
73
+ HEROKU_APPLICATION=<THE-NAME-OF-YOUR-APP-ON-HEROKU-HERE>
74
+
75
+ They are used by the [heroku-api](https://github.com/heroku/heroku.rb) gem to
76
+ scale dynos of your application.
77
+
78
+ ## Async
79
+
80
+ Whenever DynoScaler needs to scale workers up it will perform a request to the
81
+ Heroku API. This request may sometimes take longer to return than one would want.
82
+ Because of this we have a async option that uses
83
+ [GirlFriday](https://github.com/mperham/girl_friday) to handle this call
84
+ asynchronously. To enable it, just set it to `true`:
85
+
86
+ config.dyno_scaler.async = true
87
+
88
+ You can also give it a block to better customize it. It will receive an options
89
+ hash that can be passed to the `DynoScaler::Manager#scale_with` method, like so:
90
+
91
+ MY_QUEUE = GirlFriday::WorkQueue.new(:my_queue, size: 5) do |options|
92
+ DynoScaler.manager.scale_with(options)
93
+ end
94
+
95
+ config.dyno_scaler.async { MY_QUEUE << options }
96
+
97
+ ## Contributing
98
+
99
+ 1. Fork it
100
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
101
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
102
+ 4. Push to the branch (`git push origin my-new-feature`)
103
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ desc "Run all examples"
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dyno_scaler/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dyno_scaler"
8
+ gem.version = DynoScaler::VERSION
9
+ gem.authors = ["Vicente Mundim"]
10
+ gem.email = ["vicente.mundim@gmail.com"]
11
+ gem.description = %q{Scale your dyno workers on Heroku as needed}
12
+ gem.summary = %q{Scale your dyno workers on Heroku as needed}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "heroku-api"
21
+ gem.add_dependency "activesupport"
22
+
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "resque"
25
+ gem.add_development_dependency "girl_friday"
26
+ end
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+
3
+ module DynoScaler
4
+ class Configuration
5
+ ##
6
+ # Contains the max amount of workers that are allowed to run concurrently
7
+ #
8
+ # @return [Fixnum] default: 1
9
+ attr_accessor :max_workers
10
+
11
+ ##
12
+ # Contains the min amount of workers that should always be running
13
+ #
14
+ # @return [Fixnum] default: 0
15
+ attr_accessor :min_workers
16
+
17
+ ##
18
+ # Contains the ratio used to spawn more workers given a number of jobs.
19
+ #
20
+ # The given hash should have the number of workers as a key, and the number
21
+ # of queued jobs that are needed in order to spawn that number of workers
22
+ # as the value.
23
+ #
24
+ # For example, if you wanted to spawn a second worker once 6 jobs are queued
25
+ # then spawn another third worker once 10 jobs are queued you could configure
26
+ # this option as:
27
+ #
28
+ # config.job_worker_ratio = {
29
+ # 1 => 1,
30
+ # 2 => 6,
31
+ # 3 => 10
32
+ # }
33
+ #
34
+ # @param [Hash] with job worker ratio
35
+ # @return [Hash] default to { 1 => 1, 2 => 25, 3 => 50, 4 => 75, 5 => 100 }
36
+ attr_accessor :job_worker_ratio
37
+
38
+ ##
39
+ # Default is false when HEROKU_API_KEY environment variable is not set,
40
+ # otherwise defaults to true.
41
+ #
42
+ # @param [Boolean] whether to enable scaling or not
43
+ # @return [Boolean] default: false
44
+ attr_accessor :enabled
45
+ alias enabled? enabled
46
+
47
+ ##
48
+ # Default is nil when HEROKU_APP environment variable is not set,
49
+ # otherwise defaults to its value.
50
+ #
51
+ # @param [String] the name of the Heroku application used when scaling workers
52
+ # @return [String] default: nil
53
+ attr_accessor :application
54
+
55
+ def initialize
56
+ self.max_workers = 1
57
+ self.min_workers = 0
58
+ self.enabled = !ENV['HEROKU_API_KEY'].nil?
59
+ self.application = ENV['HEROKU_APP']
60
+
61
+ self.job_worker_ratio = {
62
+ 1 => 1,
63
+ 2 => 25,
64
+ 3 => 50,
65
+ 4 => 75,
66
+ 5 => 100
67
+ }
68
+ end
69
+
70
+ # Returns the current configured async Proc or configures one.
71
+ def async(&block)
72
+ @async = block if block_given?
73
+ @async
74
+ end
75
+ alias_method :async?, :async
76
+
77
+ ##
78
+ # When set to true it will use GirlFriday to asynchronous process the scaling,
79
+ # otherwise you may pass a Proc that will be called whenever scaling is needed.
80
+ #
81
+ # Defaults to false, meaning that scaling is processed synchronously.
82
+ def async=(value)
83
+ @async = value == true ? default_async_processor : value
84
+ end
85
+
86
+ ##
87
+ # The logger to be used to log message
88
+ #
89
+ # When using Rails it will default to Rails.logger, otherwise it will be
90
+ # set a `Logger.new(STDERR)`.
91
+ #
92
+ # @param [Logger] the logger to be used
93
+ # @return [Logger] default: nil
94
+ def logger
95
+ @logger ||= defined?(Rails) ? Rails.logger || Logger.new(STDERR) : Logger.new(STDERR)
96
+ end
97
+ attr_writer :logger
98
+
99
+ private
100
+ def default_async_processor
101
+ require 'girl_friday'
102
+
103
+ queue = GirlFriday::WorkQueue.new(nil, :size => 1) do |options|
104
+ DynoScaler.manager.scale_with(options)
105
+ end
106
+
107
+ Proc.new { |options| queue << options }
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module DynoScaler
4
+ class Engine < ::Rails::Engine
5
+ config.dyno_scaler = DynoScaler.configuration
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module DynoScaler
4
+ class Heroku
5
+ attr_accessor :application
6
+
7
+ def initialize(application)
8
+ self.application = application
9
+ end
10
+
11
+ def scale_workers(quantity)
12
+ heroku_client.post_ps_scale(application, 'worker', quantity)
13
+ end
14
+
15
+ protected
16
+ def heroku_client
17
+ @heroku_client ||= ::Heroku::API.new
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ module DynoScaler
4
+ class Manager
5
+ attr_accessor :options
6
+
7
+ def scale_up(options)
8
+ return unless config.enabled?
9
+
10
+ self.options = options
11
+
12
+ scale_to(number_of_workers_needed) if scale_up?
13
+ end
14
+
15
+ def scale_up?
16
+ workers_needed = number_of_workers_needed
17
+
18
+ pending_jobs? && workers_needed > options[:workers] && workers_needed <= config.max_workers
19
+ end
20
+
21
+ def scale_down(options)
22
+ return unless config.enabled?
23
+
24
+ self.options = options
25
+
26
+ scale_to(config.min_workers) if scale_down?
27
+ end
28
+
29
+ def scale_down?
30
+ options[:workers] > config.min_workers && !pending_jobs? && !working_jobs?
31
+ end
32
+
33
+ def scale_with(options)
34
+ return unless config.enabled?
35
+
36
+ action = options[:action] || action_for(options)
37
+ send(action, options) if action
38
+ end
39
+
40
+ protected
41
+ def config
42
+ DynoScaler.configuration
43
+ end
44
+
45
+ def scale_to(number_of_workers)
46
+ config.logger.info "Scaling workers to #{number_of_workers}"
47
+ heroku.scale_workers(number_of_workers)
48
+ end
49
+
50
+ def pending_jobs?
51
+ options[:pending] > 0
52
+ end
53
+
54
+ def working_jobs?
55
+ options[:working] > 0
56
+ end
57
+
58
+ def number_of_workers_needed
59
+ value = config.job_worker_ratio.reverse_each.find do |_, pending_jobs|
60
+ options[:pending] >= pending_jobs
61
+ end
62
+
63
+ value ? value.first : 0
64
+ end
65
+
66
+ def action_for(options)
67
+ self.options = options
68
+
69
+ return :scale_down if scale_down?
70
+ return :scale_up if scale_up?
71
+ end
72
+
73
+ def heroku
74
+ @heroku ||= DynoScaler::Heroku.new config.application
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module DynoScaler
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_support/concern'
4
+
5
+ module DynoScaler
6
+ module Workers
7
+ module Resque
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :scale_up_enabled
12
+ class_attribute :scale_down_enabled
13
+
14
+ enable_scaling_up
15
+ enable_scaling_down
16
+ end
17
+
18
+ module ClassMethods
19
+ def after_perform_scale_down(*args)
20
+ info = ::Resque.info
21
+ working = info[:working] > 0 ? info[:working] - 1 : 0
22
+ info.merge!(working: working) # we are not working anymore
23
+
24
+ dyno_scaler_manager.scale_down(info) if scale_down_enabled?
25
+ rescue StandardError => e
26
+ $stderr.puts "Could not scale down workers: #{e}"
27
+ end
28
+
29
+ def after_enqueue_scale_up(*args)
30
+ if scale_up_enabled?
31
+ if DynoScaler.configuration.async?
32
+ DynoScaler.configuration.async.call(::Resque.info.merge(action: :scale_up))
33
+ else
34
+ dyno_scaler_manager.scale_up(::Resque.info)
35
+ end
36
+ end
37
+ rescue StandardError => e
38
+ $stderr.puts "Could not scale up workers: #{e}"
39
+ end
40
+
41
+ def enable_scaling_up
42
+ self.scale_up_enabled = true
43
+ end
44
+
45
+ def enable_scaling_down
46
+ self.scale_down_enabled = true
47
+ end
48
+
49
+ def disable_scaling_up
50
+ self.scale_up_enabled = false
51
+ end
52
+
53
+ def disable_scaling_down
54
+ self.scale_down_enabled = false
55
+ end
56
+
57
+ private
58
+ def dyno_scaler_manager
59
+ @manager ||= DynoScaler::Manager.new
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module DynoScaler
4
+ module Workers
5
+ autoload :Resque, 'dyno_scaler/workers/resque'
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require "dyno_scaler/version"
4
+ require "active_support/core_ext/class/attribute"
5
+ require "heroku-api"
6
+
7
+ module DynoScaler
8
+ autoload :Configuration, 'dyno_scaler/configuration'
9
+ autoload :Heroku, 'dyno_scaler/heroku'
10
+ autoload :Manager, 'dyno_scaler/manager'
11
+ autoload :Workers, 'dyno_scaler/workers'
12
+
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def self.manager
18
+ @manager ||= Manager.new
19
+ end
20
+
21
+ def self.reset!
22
+ @configuration = nil
23
+ @manager = nil
24
+ end
25
+ end
26
+
27
+ require "dyno_scaler/engine" if defined?(Rails)