dyno_scaler 0.1.0

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