Permalink
Browse files

Add multiple workers support.

This allows to scale workers to more than 1, based on the job queue size, and
setting a max and min num of workers.
  • Loading branch information...
1 parent f82feac commit 3767b3d4c06973a96d6f00b7e7461bb7ab9ce07e @jaimeiniesta jaimeiniesta committed Jul 15, 2012
View
1 .rvmrc
@@ -0,0 +1 @@
+rvm use 1.9.2@workless --create
View
@@ -87,9 +87,21 @@ The local scaler uses @adamwiggins rush library http://github.com/adamwiggins/ru
The heroku scaler works on the Aspen and Bamboo stacks while the heroku_cedar scaler only works on the new Cedar stack.
+## Scaling to multiple workers
+
+As an experimental feature for the Cedar stack, Workless can scale to more than 1 worker based on the current work load. You just need to define these config variables on your app, setting the values you want:
+
+<pre>
+heroku config:add WORKLESS_MAX_WORKERS=10
+heroku config:add WORKLESS_MIN_WORKERS=0
+heroku config:add WORKLESS_WORKERS_RATIO=50
+</pre>
+
+In this example, it will scale up to a maximum of 10 workers, firing up 1 worker for every 50 jobs on the queue. The minimum will be 0 workers, but you could set it to a higher value if you want.
+
## Note on Patches/Pull Requests
-* Please fork the project, as you can see there are no tests and at present I don't know how to go about adding them so any advice would be welcome.
+* Please fork the project.
* Make your feature addition or bug fix.
* Commit, do not mess with rakefile, version, or history.
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
@@ -3,25 +3,47 @@
module Delayed
module Workless
module Scaler
-
class HerokuCedar < Base
-
extend Delayed::Workless::Scaler::HerokuClient
def self.up
- client.ps_scale(ENV['APP_NAME'], :type => 'worker', :qty => 1) if self.workers == 0
+ client.ps_scale(ENV['APP_NAME'], :type => 'worker', :qty => self.workers_needed) if self.workers < self.workers_needed
end
def self.down
- client.ps_scale(ENV['APP_NAME'], :type => 'worker', :qty => 0) unless self.workers == 0 or self.jobs.count > 0
+ client.ps_scale(ENV['APP_NAME'], :type => 'worker', :qty => self.min_workers) unless self.workers == self.min_workers or self.jobs.count > 0
end
def self.workers
client.ps(ENV['APP_NAME']).count { |p| p["process"] =~ /worker\.\d?/ }
end
- end
+ # Returns the number of workers needed based on the current number of pending jobs and the settings defined by:
+ #
+ # ENV['WORKLESS_WORKERS_RATIO']
+ # ENV['WORKLESS_MAX_WORKERS']
+ # ENV['WORKLESS_MIN_WORKERS']
+ #
+ def self.workers_needed
+ [[(self.jobs.count.to_f / self.workers_ratio).ceil, self.max_workers].min, self.min_workers].max
+ end
+
+ def self.workers_ratio
+ if ENV['WORKLESS_WORKERS_RATIO'].present? && (ENV['WORKLESS_WORKERS_RATIO'].to_i != 0)
+ ENV['WORKLESS_WORKERS_RATIO'].to_i
+ else
+ 100
+ end
+ end
+
+ def self.max_workers
+ ENV['WORKLESS_MAX_WORKERS'].present? ? ENV['WORKLESS_MAX_WORKERS'].to_i : 1
+ end
+ def self.min_workers
+ ENV['WORKLESS_MIN_WORKERS'].present? ? ENV['WORKLESS_MIN_WORKERS'].to_i : 0
+ end
+ end
end
end
-end
+end
@@ -0,0 +1,194 @@
+require 'spec_helper'
+
+describe Delayed::Workless::Scaler::HerokuCedar do
+
+ describe 'up' do
+ context 'with no workers' do
+ before(:each) do
+ Delayed::Workless::Scaler::HerokuCedar.should_receive(:workers).and_return(0)
+ end
+
+ context 'with 10 max workers' do
+ before(:each) do
+ ENV['WORKLESS_MAX_WORKERS'] = '10'
+ end
+
+ context 'with a ratio of 25 jobs per worker' do
+ before(:each) do
+ ENV['WORKLESS_WORKERS_RATIO'] = '25'
+ end
+
+ it 'should set workers to 1 for 24 jobs' do
+ if_there_are_jobs 24
+ should_scale_workers_to 1
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 1 for 25 jobs' do
+ if_there_are_jobs 25
+ should_scale_workers_to 1
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 2 for 26 jobs' do
+ if_there_are_jobs 26
+ should_scale_workers_to 2
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 3 for 51 jobs' do
+ if_there_are_jobs 51
+ should_scale_workers_to 3
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 10 for 250 jobs' do
+ if_there_are_jobs 250
+ should_scale_workers_to 10
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 10 for 5000 jobs' do
+ if_there_are_jobs 5000
+ should_scale_workers_to 10
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+ end
+
+ context 'with a ratio of 50 jobs per worker' do
+ before(:each) do
+ ENV['WORKLESS_WORKERS_RATIO'] = '50'
+ end
+
+ it 'should set workers to 1 for 50 jobs' do
+ if_there_are_jobs 50
+ should_scale_workers_to 1
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 2 for 51 jobs' do
+ if_there_are_jobs 51
+ should_scale_workers_to 2
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 10 for 500 jobs' do
+ if_there_are_jobs 500
+ should_scale_workers_to 10
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set workers to 10 for 501 jobs' do
+ if_there_are_jobs 501
+ should_scale_workers_to 10
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+ end
+ end
+ end
+
+ context 'with workers' do
+ before(:each) do
+ Delayed::Workless::Scaler::HerokuCedar.should_receive(:workers).and_return(1)
+ end
+
+ context 'with 10 max workers' do
+ before(:each) do
+ ENV['WORKLESS_MAX_WORKERS'] = '10'
+ end
+
+ context 'with a ratio of 25 jobs per worker' do
+ before(:each) do
+ ENV['WORKLESS_WORKERS_RATIO'] = '25'
+ end
+
+ it 'should not set more workers for 25 jobs' do
+ if_there_are_jobs 25
+ should_not_scale_workers
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+
+ it 'should set more workers for 26 jobs' do
+ if_there_are_jobs 26
+ should_scale_workers_to 2
+
+ Delayed::Workless::Scaler::HerokuCedar.up
+ end
+ end
+ end
+ end
+ end
+
+ describe 'down' do
+ before(:each) do
+ Delayed::Workless::Scaler::HerokuCedar.should_receive(:workers).and_return(1)
+ ENV['WORKLESS_MAX_WORKERS'] = '10'
+ ENV['WORKLESS_WORKERS_RATIO'] = '5'
+ end
+
+ context 'with 0 min workers' do
+ before(:each) do
+ ENV['WORKLESS_MIN_WORKERS'] = '0'
+ end
+
+ it 'should not scale down if there is a pending job' do
+ if_there_are_jobs 1
+ should_not_scale_workers
+
+ Delayed::Workless::Scaler::HerokuCedar.down
+ end
+
+ it 'should scale to 0 if there are no pending jobs' do
+ if_there_are_jobs 0
+ should_scale_workers_to 0
+
+ Delayed::Workless::Scaler::HerokuCedar.down
+ end
+ end
+
+ context 'with 1 min workers' do
+ before(:each) do
+ ENV['WORKLESS_MIN_WORKERS'] = '1'
+ end
+
+ it 'should not scale down if there is a pending job' do
+ if_there_are_jobs 1
+ should_not_scale_workers
+
+ Delayed::Workless::Scaler::HerokuCedar.down
+ end
+
+ it 'should not scale down even if there are no pending jobs' do
+ if_there_are_jobs 0
+ should_not_scale_workers
+
+ Delayed::Workless::Scaler::HerokuCedar.down
+ end
+ end
+ end
+
+ private
+
+ def if_there_are_jobs(num)
+ Delayed::Workless::Scaler::HerokuCedar.should_receive(:jobs).any_number_of_times.and_return(NumWorkers.new(num))
+ end
+
+ def should_scale_workers_to(num)
+ Delayed::Workless::Scaler::HerokuCedar.client.should_receive(:ps_scale).once.with(ENV['APP_NAME'], :qty => num, :type => 'worker')
+ end
+
+ def should_not_scale_workers
+ Delayed::Workless::Scaler::HerokuCedar.client.should_not_receive(:ps_scale)
+ end
+end
@@ -1,6 +1,9 @@
require 'spec_helper'
describe Delayed::Workless::Scaler::HerokuCedar do
+ before(:each) do
+ ENV['WORKLESS_MAX_WORKERS'] = ENV['WORKLESS_MIN_WORKERS'] = ENV['WORKLESS_WORKERS_RATIO'] = nil
+ end
context 'with jobs' do
@@ -24,7 +27,7 @@
context 'with workers' do
before do
- Delayed::Workless::Scaler::HerokuCedar.should_receive(:workers).and_return(NumWorkers.new(10))
+ Delayed::Workless::Scaler::HerokuCedar.should_receive(:workers).and_return(10)
end
it 'should not set anything' do

0 comments on commit 3767b3d

Please sign in to comment.