resque-heroku-scaling-canary 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'resque', '~> 1.9'
4
+ gem 'heroku', '~> 1.11'
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.7"
10
+ gem "rspec-mocks", "~> 2.7"
11
+ gem "jeweler", "~> 1.6.4"
12
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Art.sy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ resque-heroku-scaling-canary
2
+ ============================
3
+
4
+ This gem defines a Resque plugin that allows you to automatically
5
+ scale up the number of workers running on Heroku and then automatically scale them down once
6
+ no work is left to do. To use, extend the module from your job:
7
+
8
+ class MyJob
9
+ extend Resque::Plugins::ScalingCanary
10
+
11
+ def self.minimum_workers_needed
12
+ 10
13
+ end
14
+
15
+ def self.perform
16
+ ...
17
+ end
18
+ end
19
+
20
+ Defining `minimum_workers_needed` like we did above is optional, but if you don't define it, it
21
+ defaults to 1. ScalingCanary makes sure that when your job is enqueued there are at least this
22
+ many workers working to service it.
23
+
24
+ Next, define the environment variables HEROKU_USER, HEROKU_PASSWORD, and HEROKU_APP with your
25
+ heroku credentials and app name:
26
+
27
+ $ heroku config:add HEROKU_USER=aaron@art.sy
28
+ $ heroku config:add HEROKU_PASSWORD=5u93r53cr37
29
+ $ heroku config:add HEROKU_APP=awesome-app
30
+
31
+ When you enqueue your job, you'll see a new queue called "~scaling-canary" created with a single
32
+ job in it. This job is the canary - its queue name is lexicographically after any other queue
33
+ names you have, so it'll get processed last. When it runs, it looks around to see if any other
34
+ jobs are being worked on or are awaiting workers on any queues. If any are, it requeues itself,
35
+ but if everything's finished, it shuts down all workers.
36
+
37
+ There are a few other gems that allow you to automatically scale up the Heroku workers you
38
+ use in response to your Resque load and then kill those workers automatically when
39
+ the work is done: [resque-heroku-autoscaler](https://github.com/ajmurmann/resque-heroku-autoscaler)
40
+ and [hirefire](https://github.com/meskyanichi/hirefire) are two notable examples. This gem is
41
+ takes a dumber but more easily auditable approach than either of the above alternatives that's
42
+ better suited to systems running a largish set of batch jobs that might spawn other Resque jobs.
43
+ In particular, this gem is meant to work well with jobs using
44
+ [resque-multi-step](https://github.com/pezra/resque-multi-step), which enqueues finalization
45
+ jobs from within Resque tasks in a way that sometimes confuses other auto-scaling gems.
46
+
47
+ This plugin works with Resque version 1.9 and above.
48
+
49
+ Installation:
50
+ -------------
51
+
52
+ gem install resque_heroku_scaling_canary
53
+
54
+ Configuration:
55
+ --------------
56
+
57
+ You can configure the following parameters:
58
+
59
+ * `heroku_user`: defaults to the value of the environment variable HEROKU_USER
60
+ * `heroku_password`: defaults to the value of the environment variable HEROKU_PASSWORD
61
+ * `heroku_app`: defaults to the value of the environment variable HEROKU_APP
62
+ * `polling_interval`: the polling interval, in seconds, that the canary should wait
63
+ between checking the outstanding Resque jobs and working Resque workers. To be
64
+ safe, the canary checks these values twice, waiting for `polling_interval` seconds
65
+ in between, before shutting all workers down.
66
+ * `disable_scaling_if`: called with a block, will evaluate the block and disable
67
+ scaling entirely if it evaluates to `true`.
68
+
69
+ These values are easiest to configure in an initializer, for example, create the
70
+ file config/initializers/resque_heroku_scaling_canary.rb and put something like the
71
+ following in the file:
72
+
73
+ require 'resque_heroku_scaling_canary'
74
+
75
+ Resque::Plugins::ScalingCanary.config do |config|
76
+ config.heroku_app = "myapp"
77
+ config.polling_interval = 3
78
+ config.disable_scaling_if{ Rails.env == 'development' }
79
+ end
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "resque-heroku-scaling-canary"
18
+ gem.homepage = "http://github.com/aaw/resque-heroku-scaling-canary"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Auto-scale Heroku workers for Resque}
21
+ gem.description = %Q{Auto-scale Heroku workers for Resque}
22
+ gem.email = "aaron.windsor@gmail.com"
23
+ gem.authors = ["Aaron Windsor"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,22 @@
1
+ require "resque/plugins/resque_heroku_scaling_canary/config"
2
+ require "resque/plugins/resque_heroku_scaling_canary/canary_job"
3
+
4
+ module Resque
5
+ module Plugins
6
+ module ScalingCanary
7
+
8
+ def after_enqueue_ensure_heroku_workers(*args)
9
+ return if Config.scaling_disabled?
10
+ n = self.respond_to?(:minimum_workers_needed) ? self.minimum_workers_needed : 1
11
+ return if Config.heroku_client.info(Config.heroku_app)[:workers].to_i >= n
12
+ Config.heroku_client.set_workers(n)
13
+ Resque.enqueue(CanaryJob, Config.polling_interval) if CanaryJob.canary_jobs_pending == 0
14
+ end
15
+
16
+ def self.config
17
+ yield Config
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ require "resque/plugins/resque_heroku_scaling_canary/config"
2
+
3
+ module Resque
4
+ module Plugins
5
+ module ScalingCanary
6
+ class CanaryJob
7
+
8
+ @queue = "~scaling-canary"
9
+
10
+ def self.perform(timeout)
11
+ before = self.non_canary_jobs_pending
12
+ Kernel.sleep Config.polling_interval
13
+ after = self.non_canary_jobs_pending
14
+ if before == after and before == 0
15
+ Config.heroku_client.set_workers(0)
16
+ else
17
+ Resque.enqueue(self, timeout)
18
+ end
19
+ end
20
+
21
+ def self.canary_jobs_pending
22
+ Resque.size(@queue)
23
+ end
24
+
25
+ def self.non_canary_jobs_pending
26
+ waiting = Resque.queues.inject(0){ |accum, item| accum += Resque.size(item) unless item == @queue }
27
+ being_processed = Resque.workers.find_all{ |w| w.processing["queue"] and w.processing["queue"] != @queue }.count
28
+ waiting + being_processed
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Resque
2
+ module Plugins
3
+ module ScalingCanary
4
+ module Config
5
+ extend self
6
+
7
+ @disable_scaling_if = lambda { false }
8
+ def disable_scaling_if(&block)
9
+ @disable_scaling_if = block
10
+ end
11
+
12
+ def scaling_disabled?
13
+ @disable_scaling_if.call
14
+ end
15
+
16
+ attr_writer :heroku_user
17
+ def heroku_user
18
+ @heroku_user ||= ENV['HEROKU_USER']
19
+ end
20
+
21
+ attr_writer :heroku_password
22
+ def heroku_password
23
+ @heroku_pass ||= ENV['HEROKU_PASS']
24
+ end
25
+
26
+ attr_writer :heroku_app
27
+ def heroku_app
28
+ @heroku_app ||= ENV['HEROKU_APP']
29
+ end
30
+
31
+ attr_writer :polling_interval
32
+ def polling_interval
33
+ @polling_interval ||= 5
34
+ end
35
+
36
+ def heroku_client
37
+ @@heroku_client ||= Heroku::Client.new(Config.heroku_user, Config.heroku_password)
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "resque-heroku-scaling-canary"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Aaron Windsor"]
12
+ s.date = "2012-01-30"
13
+ s.description = "Auto-scale Heroku workers for Resque"
14
+ s.email = "aaron.windsor@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "LICENSE.txt",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "lib/resque/plugins/resque_heroku_scaling_canary.rb",
26
+ "lib/resque/plugins/resque_heroku_scaling_canary/canary_job.rb",
27
+ "lib/resque/plugins/resque_heroku_scaling_canary/config.rb",
28
+ "resque-heroku-scaling-canary.gemspec",
29
+ "spec/config_spec.rb",
30
+ "spec/resque_heroku_scaling_canary_spec.rb",
31
+ "spec/spec_helper.rb"
32
+ ]
33
+ s.homepage = "http://github.com/aaw/resque-heroku-scaling-canary"
34
+ s.licenses = ["MIT"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = "1.8.10"
37
+ s.summary = "Auto-scale Heroku workers for Resque"
38
+
39
+ if s.respond_to? :specification_version then
40
+ s.specification_version = 3
41
+
42
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
43
+ s.add_runtime_dependency(%q<resque>, ["~> 1.9"])
44
+ s.add_runtime_dependency(%q<heroku>, ["~> 1.11"])
45
+ s.add_development_dependency(%q<rspec>, ["~> 2.7"])
46
+ s.add_development_dependency(%q<rspec-mocks>, ["~> 2.7"])
47
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
48
+ else
49
+ s.add_dependency(%q<resque>, ["~> 1.9"])
50
+ s.add_dependency(%q<heroku>, ["~> 1.11"])
51
+ s.add_dependency(%q<rspec>, ["~> 2.7"])
52
+ s.add_dependency(%q<rspec-mocks>, ["~> 2.7"])
53
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
54
+ end
55
+ else
56
+ s.add_dependency(%q<resque>, ["~> 1.9"])
57
+ s.add_dependency(%q<heroku>, ["~> 1.11"])
58
+ s.add_dependency(%q<rspec>, ["~> 2.7"])
59
+ s.add_dependency(%q<rspec-mocks>, ["~> 2.7"])
60
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
61
+ end
62
+ end
63
+
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe Resque::Plugins::ScalingCanary::Config do
4
+ describe "scaling_disabled?" do
5
+ it "returns false by default" do
6
+ subject.scaling_disabled?.should be_false
7
+ end
8
+ it "returns the results of a call to disable_scaling_if when an implementation is provided" do
9
+ subject.disable_scaling_if{ true }
10
+ subject.scaling_disabled?.should be_true
11
+ subject.disable_scaling_if{ false }
12
+ subject.scaling_disabled?.should be_false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ class SmallBatchJob
4
+ extend Resque::Plugins::ScalingCanary
5
+ @queue = 'small-batch-job'
6
+
7
+ def self.perform; end
8
+ end
9
+
10
+ class LargeBatchJob
11
+ extend Resque::Plugins::ScalingCanary
12
+ @queue = 'large-batch-job'
13
+
14
+ def self.minimum_workers_needed
15
+ 20
16
+ end
17
+
18
+ def self.perform; end
19
+ end
20
+
21
+ describe Resque::Plugins::ScalingCanary do
22
+ before(:each) do
23
+ @mock_heroku_client = Object.new
24
+ Resque::Plugins::ScalingCanary.config do |config|
25
+ config.heroku_user = "stub"
26
+ config.heroku_password = "stub"
27
+ config.heroku_app = "stub"
28
+ config.polling_interval = 0
29
+ config.stub(:heroku_client) { @mock_heroku_client }
30
+ end
31
+ end
32
+
33
+ it "should pass Resque's plugin lint test" do
34
+ lambda { Resque::Plugin.lint(Resque::Plugins::ScalingCanary) }.should_not raise_error
35
+ end
36
+
37
+ describe "after_enqueue_ensure_heroku_workers" do
38
+ it "should scale workers up to 1 if no :minimum_workers_needed method is implemented" do
39
+ @mock_heroku_client.should_receive(:set_workers).with(1).once
40
+ @mock_heroku_client.should_receive(:set_workers).with(0).once
41
+ @mock_heroku_client.stub(:info) { {:workers => 0} }
42
+ Resque.enqueue SmallBatchJob
43
+ end
44
+ it "should scale workers up to the number specified by :minimum_workers_needed if an implementation is provided" do
45
+ @mock_heroku_client.should_receive(:set_workers).with(20).once
46
+ @mock_heroku_client.should_receive(:set_workers).with(0).once
47
+ @mock_heroku_client.stub(:info) { {:workers => 0} }
48
+ Resque.enqueue LargeBatchJob
49
+ end
50
+ it "should not scale workers at all if scaling is disabled" do
51
+ Resque::Plugins::ScalingCanary::Config.disable_scaling_if{ true }
52
+ @mock_heroku_client.stub(:set_workers){ raise "Not expecting call to :set_workers because scaling is disabled" }
53
+ @mock_heroku_client.stub(:info) { {:workers => 0} }
54
+ Resque.enqueue LargeBatchJob
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,12 @@
1
+ require 'rspec'
2
+ require 'heroku'
3
+ require 'resque'
4
+ require 'resque/plugins/resque_heroku_scaling_canary/config'
5
+ require 'resque/plugins/resque_heroku_scaling_canary'
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :rspec
9
+ config.expect_with :rspec
10
+ end
11
+
12
+ Resque.inline = true
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-heroku-scaling-canary
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Aaron Windsor
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-30 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: resque
16
+ requirement: &2164966460 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2164966460
25
+ - !ruby/object:Gem::Dependency
26
+ name: heroku
27
+ requirement: &2164965560 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.11'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2164965560
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2164964420 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '2.7'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2164964420
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-mocks
49
+ requirement: &2164963280 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.7'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2164963280
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &2164962200 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.6.4
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2164962200
69
+ description: Auto-scale Heroku workers for Resque
70
+ email: aaron.windsor@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - LICENSE.txt
75
+ - README.md
76
+ files:
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - VERSION
82
+ - lib/resque/plugins/resque_heroku_scaling_canary.rb
83
+ - lib/resque/plugins/resque_heroku_scaling_canary/canary_job.rb
84
+ - lib/resque/plugins/resque_heroku_scaling_canary/config.rb
85
+ - resque-heroku-scaling-canary.gemspec
86
+ - spec/config_spec.rb
87
+ - spec/resque_heroku_scaling_canary_spec.rb
88
+ - spec/spec_helper.rb
89
+ homepage: http://github.com/aaw/resque-heroku-scaling-canary
90
+ licenses:
91
+ - MIT
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ segments:
103
+ - 0
104
+ hash: 2123918992578808626
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.10
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Auto-scale Heroku workers for Resque
117
+ test_files: []