resque-heroku-scaling-canary 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/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: []