resque-director 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "json"
4
+ gem 'resque'
5
+
6
+ group :development do
7
+ gem "rspec", "~> 2.3.0"
8
+ gem "bundler", "~> 1.0.0"
9
+ gem "jeweler", "~> 1.6.4"
10
+ gem "rcov", ">= 0"
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Nolan Frausto
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.rdoc ADDED
@@ -0,0 +1,76 @@
1
+ = Resque Director
2
+
3
+ Resque Director is a plugin for the Resque queueing system (http://github.com/defunkt/resque) that "directs" workers on a queue by automatically adding or removing workers from a queue based on how backed up a queue becomes, or how long it takes for a job to get pulled off the queue.
4
+
5
+ ==About
6
+
7
+ resque-director is mainly useful for when you are managing a large number of workers and don't want to waste resources keeping all of them waiting when they are not being used. Also useful in queues where the influx of jobs can change dramatically from time to time: enabling more workers during the times when the queue is filling up more quickly, and less in the opposite scenario. Different queues can be given different directions as well.
8
+
9
+ == Usage
10
+
11
+ When creating your jobs you should include (make sure to include not extend) Resque::Plugins::Director and add direction options.
12
+
13
+ For Example:
14
+
15
+ class Job
16
+ include Resque::Plugins::Director
17
+ direct :min_workers => 2, :max_workers => 4, :max_time => 60, :max_queue => 10, :wait_time => 30
18
+ @queue = :test
19
+
20
+ #rest of your Job class here
21
+ end
22
+
23
+
24
+ === Configuration Options
25
+
26
+ <b>min_workers</b>:: specifies the minimum number of workers running at any point in time. If there are no workers running or less than the minimum running it will start as many workers necessary to get it to the minimum. The default is 1.
27
+
28
+ <b>max_workers</b>:: specifies the maximum number of workers running at any point in time. It will never start more than the maximum number of workers. If anything less than or equal to zero is specified as the maximum it will be treated as if there is no maximum, and theoretically an infinite number of workers could be added. The default is 0.
29
+
30
+ <b>max_time</b>:: the maximum time in seconds that a job takes to get pulled off the queue, if a job takes longer than this time then a worker is added. If anything less than or equal to zero is specified as the maximum time, this field will be ignored. The default is 0.
31
+
32
+ <b>max_queue</b>:: the maximum jobs that can build up in a queue, if more than this number of jobs build up then a worker is added. If anything less than or equal to zero is specified as the maximum queue, this field will be ignored. The default is 0.
33
+
34
+ <b>wait_time</b>:: the time that it will wait after adding or removing a worker before being allowed to add or remove workers again. The default is 60 seconds.
35
+
36
+ == Worker Options
37
+
38
+ <b>start_override</b>:: This will run exactly what you put in the command override as a system command to start A SINGLE worker, allowing you to customize the starting of a worker fully. The system command "QUEUE=queue_name rake resque:work" is run by default, where queue_name is whatever queue the job is running.
39
+
40
+ <b>stop_override</b>:: This will run exactly what you put in the command override as a system command to stop A SINGLE worker, allowing you to customize the stoping of a worker fully. Process.kill("QUIT", worker_pid) is used to stop the worker by default, where worker_pid is the PID of a worker.
41
+
42
+ === Starting/Stopping Workers Example
43
+
44
+ class Job
45
+ include Resque::Plugins::Director
46
+ direct :start_override => "./start_command_to_run", :stop_override => "./stop_command_to_run"
47
+ @queue = :test
48
+
49
+ #rest of your Job class here
50
+ end
51
+
52
+ === Conditions For Removing Workers
53
+
54
+ A worker will be removed if the jobs in the queue fall below half of the <b>max_queue</b>, or if the time it takes for a job to be pulled off of a queue falls below half of the <b>max_time</b>. Workers will be scaled down to the minimum if there are no jobs on the queue.
55
+
56
+ === Special Cases
57
+
58
+ * If a max_worker is less than min_worker then the default for max_worker will be used (there will be no maximum).
59
+ * If a min_workers is set to anything less than 1 then it will be treated as 0.
60
+
61
+
62
+ == Contributing to resque-reconnect
63
+
64
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
65
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
66
+ * Fork the project
67
+ * Start a feature/bugfix branch
68
+ * Commit and push until you are happy with your contribution
69
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
70
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
71
+
72
+ == Copyright
73
+
74
+ Copyright (c) 2011 Nolan Frausto. See LICENSE.txt for
75
+ further details.
76
+
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ $LOAD_PATH.unshift 'lib'
3
+
4
+ require 'rubygems'
5
+ require 'bundler'
6
+ require 'resque/tasks'
7
+
8
+ begin
9
+ Bundler.setup(:default, :development)
10
+ rescue Bundler::BundlerError => e
11
+ $stderr.puts e.message
12
+ $stderr.puts "Run `bundle install` to install missing gems"
13
+ exit e.status_code
14
+ end
15
+ require 'rake'
16
+
17
+ require 'jeweler'
18
+ Jeweler::Tasks.new do |gem|
19
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
20
+ gem.name = "resque-director"
21
+ gem.homepage = "http://github.com/frausto/resque-director"
22
+ gem.license = "MIT"
23
+ gem.summary = %Q{resque plugin for dynamically adding/removing workers to a queue}
24
+ gem.description = %Q{resque plugin for dynamically adding/removing workers to a queue}
25
+ gem.email = "nrfrausto@gmail.com"
26
+ gem.authors = ["Nolan Frausto"]
27
+ # dependencies defined in Gemfile
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
30
+
31
+ require 'rspec/core'
32
+ require 'rspec/core/rake_task'
33
+ RSpec::Core::RakeTask.new(:spec) do |spec|
34
+ spec.pattern = FileList['spec/**/*_spec.rb']
35
+ end
36
+
37
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
38
+ spec.pattern = 'spec/**/*_spec.rb'
39
+ spec.rcov = true
40
+ end
41
+
42
+ task :default => :spec
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "resque-director #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,39 @@
1
+ module Resque
2
+ module Plugins
3
+ module Director
4
+ module Config
5
+ extend self
6
+
7
+ attr_accessor :queue
8
+
9
+ DEFAULT_OPTIONS = {
10
+ :min_workers => 1,
11
+ :max_workers => 0,
12
+ :max_time => 0,
13
+ :max_queue => 0,
14
+ :wait_time => 60,
15
+ :start_override => nil,
16
+ :stop_override => nil
17
+ }
18
+
19
+ def reset!
20
+ DEFAULT_OPTIONS.each do |key, default|
21
+ attr_reader key
22
+ self.instance_variable_set("@#{key.to_s}", default)
23
+ end
24
+ end
25
+
26
+ def setup(options={})
27
+ DEFAULT_OPTIONS.each do |key, value|
28
+ self.instance_variable_set("@#{key.to_s}", options[key] || value)
29
+ end
30
+
31
+ @min_workers = 0 if @min_workers < 0
32
+ @max_workers = DEFAULT_OPTIONS[:max_workers] if @max_workers < @min_workers
33
+ end
34
+
35
+ reset!
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ module Resque
2
+ module Plugins
3
+ module Director
4
+ module Lifecycle
5
+
6
+ def self.included(base) #:nodoc:
7
+ base.class_eval do
8
+ alias_method :push_without_lifecycle, :push
9
+ extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def push(queue, item)
16
+ if item.respond_to?(:[]=)
17
+ timestamp = {'resdirecttime' => Time.now.utc.to_i}
18
+ item[:args] = item[:args].push(timestamp)
19
+ end
20
+ push_without_lifecycle queue, item
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ module Resque
29
+ include Resque::Plugins::Director::Lifecycle
30
+ end
@@ -0,0 +1,72 @@
1
+ module Resque
2
+ module Plugins
3
+ module Director
4
+ class Scaler
5
+ class << self
6
+
7
+ def scale_up(number_of_workers=1)
8
+ number_of_workers = WorkerTracker.new.total_to_add(number_of_workers)
9
+ scaling(number_of_workers) do
10
+ number_of_workers.times { start }
11
+ end
12
+ end
13
+
14
+ def scale_down(number_of_workers=1)
15
+ tracker = WorkerTracker.new
16
+ number_of_workers = tracker.total_to_remove(number_of_workers)
17
+
18
+ scaling(number_of_workers) do
19
+ stop(tracker, number_of_workers)
20
+ end
21
+ end
22
+
23
+ def scale_down_to_minimum
24
+ tracker = WorkerTracker.new
25
+ number_of_workers = tracker.total_to_go_to_minimum
26
+ stop(tracker, number_of_workers)
27
+ end
28
+
29
+ def scale_within_requirements
30
+ number_of_workers = WorkerTracker.new.total_for_requirements
31
+
32
+ if number_of_workers > 0
33
+ scale_up(number_of_workers)
34
+ elsif number_of_workers < 0
35
+ scale_down(number_of_workers * -1)
36
+ end
37
+ end
38
+
39
+ def scaling(number_of_workers=1)
40
+ return unless time_to_scale? && number_of_workers > 0
41
+ yield
42
+ Resque.redis.set("last_scaled_#{Config.queue}", Time.now.utc.to_i)
43
+ end
44
+
45
+ private
46
+
47
+ def time_to_scale?
48
+ last_time = Resque.redis.get("last_scaled_#{Config.queue}")
49
+ return true if last_time.nil?
50
+ time_passed = (Time.now.utc - Time.at(last_time.to_i).utc)
51
+ time_passed >= Config.wait_time
52
+ end
53
+
54
+ def start
55
+ default_command = "QUEUE=#{Config.queue} rake resque:work &"
56
+ system(Config.start_override || default_command)
57
+ end
58
+
59
+ def stop(tracker, number_of_workers)
60
+ if Config.stop_override
61
+ number_of_workers.times { system(Config.stop_override) }
62
+ else
63
+ valid_workers = tracker.workers.select{|w| w.hostname == `hostname`.chomp}
64
+ worker_pids = valid_workers[0...number_of_workers].map(&:pid)
65
+ worker_pids.each {|pid| Process.kill("QUIT", pid)}
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,57 @@
1
+ module Resque
2
+ module Plugins
3
+ module Director
4
+ class WorkerTracker
5
+ attr_reader :workers
6
+
7
+ def initialize
8
+ @workers = current_workers
9
+ @number_working = @workers.size
10
+ end
11
+
12
+ def total_for_requirements
13
+ start_number = workers_to_start
14
+ stop_number = workers_to_stop
15
+ return start_number if start_number > 0
16
+ return stop_number if stop_number < 0
17
+ 0
18
+ end
19
+
20
+ def total_to_go_to_minimum
21
+ to_minimum = @number_working - Config.min_workers
22
+ to_minimum > 0 ? to_minimum : 0
23
+ end
24
+
25
+ def total_to_add(number_to_start)
26
+ return number_to_start if Config.max_workers <= 0
27
+ scale_limit = Config.max_workers - @number_working
28
+ number_to_start > scale_limit ? scale_limit : number_to_start
29
+ end
30
+
31
+ def total_to_remove(number_to_stop)
32
+ min_workers = Config.min_workers <= 0 ? 1 : Config.min_workers
33
+ scale_limit = @number_working - min_workers
34
+ number_to_stop > scale_limit ? scale_limit : number_to_stop
35
+ end
36
+
37
+ private
38
+
39
+ def workers_to_start
40
+ min_workers = Config.min_workers <= 0 ? 1 : Config.min_workers
41
+ workers_to_start = min_workers - @number_working
42
+ end
43
+
44
+ def workers_to_stop
45
+ return 0 if Config.max_workers <= 0
46
+ workers_to_stop = Config.max_workers - @number_working
47
+ end
48
+
49
+ def current_workers
50
+ Resque.workers.select do |w|
51
+ w.queues == [Config.queue] && !w.shutdown?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,95 @@
1
+ module Resque
2
+ module Plugins
3
+ module Director
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.overwrite_perform
8
+ base.instance_eval do
9
+ def singleton_method_added(name)
10
+ return if name != :perform
11
+ overwrite_perform
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def direct(options={})
18
+ Config.setup(options)
19
+ end
20
+
21
+ def overwrite_perform
22
+ class_eval do |klass|
23
+ if klass.respond_to?('perform') && !klass.respond_to?('custom_perform')
24
+ klass.instance_eval do
25
+ def custom_perform(*args)
26
+ args.pop unless retrieve_timestamp(args.last).nil?
27
+ original_perform(*args)
28
+ end
29
+ end
30
+
31
+ class << klass
32
+ alias_method :original_perform, :perform
33
+ alias_method :perform, :custom_perform
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def after_enqueue_scale_workers(*args)
40
+ Config.queue = @queue.to_s
41
+ Scaler.scale_within_requirements
42
+ end
43
+
44
+ def before_perform_direct_workers(*args)
45
+ return unless scaling_config_set?
46
+ Config.queue = @queue.to_s
47
+ time_stamp = retrieve_timestamp(args.pop)
48
+ start_time = time_stamp.nil? ? Time.now.utc : Time.at(time_stamp.to_i).utc
49
+
50
+ time_through_queue = Time.now.utc - start_time
51
+ jobs_in_queue = Resque.size(@queue.to_s)
52
+
53
+ if scale_up?(time_through_queue, jobs_in_queue)
54
+ Scaler.scale_up
55
+ elsif scale_down?(time_through_queue, jobs_in_queue)
56
+ Scaler.scale_down
57
+ end
58
+ end
59
+
60
+ def after_perform_direct_workers(*args)
61
+ jobs_in_queue = Resque.size(@queue.to_s)
62
+ Scaler.scale_down_to_minimum if jobs_in_queue == 0
63
+ end
64
+
65
+ def on_failure_direct_workers(*args)
66
+ jobs_in_queue = Resque.size(@queue.to_s)
67
+ Scaler.scale_down_to_minimum if jobs_in_queue == 0
68
+ end
69
+
70
+ private
71
+
72
+ def retrieve_timestamp(timestamp)
73
+ return nil unless timestamp.class.to_s == "Hash"
74
+ timestamp['resdirecttime'] || timestamp[:resdirecttime]
75
+ end
76
+
77
+ def scaling_config_set?
78
+ Config.max_time > 0 || Config.max_queue > 0
79
+ end
80
+
81
+ def scale_up?(time_through_queue, jobs_in_queue)
82
+ time_limits = Config.max_time > 0 && time_through_queue > Config.max_time
83
+ queue_limits = Config.max_queue > 0 && jobs_in_queue > Config.max_queue
84
+ time_limits || queue_limits
85
+ end
86
+
87
+ def scale_down?(time_through_queue, jobs_in_queue)
88
+ time_limits = Config.max_time > 0 && time_through_queue < (Config.max_time/2)
89
+ queue_limits = Config.max_queue > 0 && jobs_in_queue < (Config.max_queue/2)
90
+ (Config.max_time <= 0 || time_limits) && (Config.max_queue <= 0 || queue_limits)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,7 @@
1
+ require 'resque'
2
+
3
+ require 'resque/plugins/director'
4
+ require 'resque/plugins/director/worker_tracker'
5
+ require 'resque/plugins/director/config'
6
+ require 'resque/plugins/director/scaler'
7
+ require 'resque/plugins/director/lifecycle'
@@ -0,0 +1,76 @@
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 = %q{resque-director}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Nolan Frausto}]
12
+ s.date = %q{2011-08-20}
13
+ s.description = %q{resque plugin for dynamically adding/removing workers to a queue}
14
+ s.email = %q{nrfrausto@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "lib/resque-director.rb",
28
+ "lib/resque/plugins/director.rb",
29
+ "lib/resque/plugins/director/config.rb",
30
+ "lib/resque/plugins/director/lifecycle.rb",
31
+ "lib/resque/plugins/director/scaler.rb",
32
+ "lib/resque/plugins/director/worker_tracker.rb",
33
+ "resque-director.gemspec",
34
+ "spec/redis-test.conf",
35
+ "spec/resque/plugins/director/config_spec.rb",
36
+ "spec/resque/plugins/director/lifecycle_spec.rb",
37
+ "spec/resque/plugins/director/scaler_spec.rb",
38
+ "spec/resque/plugins/director/worker_tracker_spec.rb",
39
+ "spec/resque/plugins/director_spec.rb",
40
+ "spec/spec_helper.rb",
41
+ "spec/support/test_job.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/frausto/resque-director}
44
+ s.licenses = [%q{MIT}]
45
+ s.require_paths = [%q{lib}]
46
+ s.rubygems_version = %q{1.8.8}
47
+ s.summary = %q{resque plugin for dynamically adding/removing workers to a queue}
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<json>, [">= 0"])
54
+ s.add_runtime_dependency(%q<resque>, [">= 0"])
55
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
56
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
57
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
58
+ s.add_development_dependency(%q<rcov>, [">= 0"])
59
+ else
60
+ s.add_dependency(%q<json>, [">= 0"])
61
+ s.add_dependency(%q<resque>, [">= 0"])
62
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
63
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
64
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
65
+ s.add_dependency(%q<rcov>, [">= 0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<json>, [">= 0"])
69
+ s.add_dependency(%q<resque>, [">= 0"])
70
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
71
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
72
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
73
+ s.add_dependency(%q<rcov>, [">= 0"])
74
+ end
75
+ end
76
+