hirefire 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,8 +31,20 @@ module HireFire
31
31
  # # It'll match at { :jobs => 35, :workers => 3 }, (35 jobs or more: hire 3 workers)
32
32
  # # meaning that it'll ensure there are 3 workers running.
33
33
  #
34
- # # If there were already were 3 workers, it'll leave it as is
35
- #
34
+ # # If there were already were 3 workers, it'll leave it as is.
35
+ #
36
+ # # Alternatively, you can use a more functional syntax, which works in the same way.
37
+ #
38
+ # HireFire.configure do |config|
39
+ # config.max_workers = 5
40
+ # config.job_worker_ratio = [
41
+ # { :when => lambda {|jobs| jobs < 15 }, :workers => 1 },
42
+ # { :when => lambda {|jobs| jobs < 35 }, :workers => 2 },
43
+ # { :when => lambda {|jobs| jobs < 60 }, :workers => 3 },
44
+ # { :when => lambda {|jobs| jobs < 80 }, :workers => 4 }
45
+ # ]
46
+ # end
47
+ #
36
48
  # # If there were more than 3 workers running (say, 4 or 5), it will NOT reduce
37
49
  # # the number. This is because when you reduce the number of workers, you cannot
38
50
  # # tell which worker Heroku will shut down, meaning you might interrupt a worker
@@ -50,16 +62,97 @@ module HireFire
50
62
  jobs_count = jobs
51
63
  workers_count = workers
52
64
 
53
- ratio.each do |ratio|
54
- if jobs_count >= ratio[:jobs] and max_workers >= ratio[:workers]
55
- if not workers_count == ratio[:workers]
56
- Logger.message("Hiring more workers so we have #{ ratio[:workers] } in total.")
57
- workers(ratio[:workers])
65
+ ##
66
+ # Use "Standard Notation"
67
+ if not ratio.first[:when].respond_to? :call
68
+
69
+ ##
70
+ # Since the "Standard Notation" is defined in the in an ascending order
71
+ # in the array of hashes, we need to reverse this order in order to properly
72
+ # loop through and break out of the array at the correctly matched ratio
73
+ ratio.reverse!
74
+
75
+ ##
76
+ # Iterates through all the defined job/worker ratio's
77
+ # until it finds a match. Then it hires (if necessary) the appropriate
78
+ # amount of workers and breaks out of the loop
79
+ ratio.each do |ratio|
80
+
81
+ ##
82
+ # Standard notation
83
+ # This is the code for the default notation
84
+ #
85
+ # @example
86
+ # { :jobs => 35, :workers => 3 }
87
+ #
88
+ if jobs_count >= ratio[:jobs] and max_workers >= ratio[:workers]
89
+ if workers_count < ratio[:workers]
90
+ log_and_hire(ratio[:workers])
91
+ end
92
+
93
+ return
58
94
  end
95
+ end
96
+
97
+ ##
98
+ # If no match is found in the above job/worker ratio loop, then we'll
99
+ # perform one last operation to see whether the the job count is greater
100
+ # than the highest job/worker ratio, and if this is the case then we also
101
+ # check to see whether the maximum amount of allowed workers is greater
102
+ # than the amount that are currently running, if this is the case, we are
103
+ # allowed to hire the max amount of workers.
104
+ if jobs_count >= ratio.first[:jobs] and max_workers > workers_count
105
+ log_and_hire(max_workers)
106
+ return
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Use "Functional (Lambda) Notation"
112
+ if ratio.first[:when].respond_to? :call
113
+
114
+ ##
115
+ # Iterates through all the defined job/worker ratio's
116
+ # until it finds a match. Then it hires (if necessary) the appropriate
117
+ # amount of workers and breaks out of the loop
118
+ ratio.each do |ratio|
59
119
 
60
- break
120
+ ##
121
+ # Functional (Lambda) Notation
122
+ # This is the code for the Lambda notation, more verbose,
123
+ # but more humanly understandable
124
+ #
125
+ # @example
126
+ # { :when => lambda {|jobs| jobs < 60 }, :workers => 3 }
127
+ #
128
+ if ratio[:when].call(jobs_count) and max_workers >= ratio[:workers]
129
+ if workers_count < ratio[:workers]
130
+ log_and_hire(ratio[:workers])
131
+ end
132
+
133
+ break
134
+ end
61
135
  end
62
136
  end
137
+
138
+ ##
139
+ # Applies only to the Functional (Lambda) Notation
140
+ # If the amount of queued jobs exceeds that of which was defined in the
141
+ # job/worker ratio array, it will hire the maximum amount of workers
142
+ #
143
+ # "if statements":
144
+ # 1. true if we use the Functional (Lambda) Notation
145
+ # 2. true if the last ratio (highest job/worker ratio) was exceeded
146
+ # 3. true if the max amount of workers are not yet running
147
+ #
148
+ # If all the the above statements are true, HireFire will hire the maximum
149
+ # amount of workers that were specified in the configuration
150
+ #
151
+ if ratio.last[:when].respond_to? :call \
152
+ and ratio.last[:when].call(jobs_count) === false \
153
+ and max_workers != workers_count
154
+ log_and_hire(max_workers)
155
+ end
63
156
  end
64
157
 
65
158
  ##
@@ -79,6 +172,15 @@ module HireFire
79
172
 
80
173
  private
81
174
 
175
+ ##
176
+ # Helper method for hire that logs the hiring of more workers, then hires those workers.
177
+ #
178
+ # @return [nil]
179
+ def log_and_hire(amount)
180
+ Logger.message("Hiring more workers so we have #{ amount } in total.")
181
+ workers(amount)
182
+ end
183
+
82
184
  ##
83
185
  # Wrapper method for HireFire.configuration
84
186
  # Returns the max amount of workers that may run concurrently
@@ -92,9 +194,9 @@ module HireFire
92
194
  # Wrapper method for HireFire.configuration
93
195
  # Returns the job/worker ratio array (in reversed order)
94
196
  #
95
- # @return [Array] the array of hashes containing the job/worker ratio (in reversed order)
197
+ # @return [Array] the array of hashes containing the job/worker ratio
96
198
  def ratio
97
- HireFire.configuration.job_worker_ratio.reverse
199
+ HireFire.configuration.job_worker_ratio
98
200
  end
99
201
 
100
202
  end
@@ -4,33 +4,69 @@ module HireFire
4
4
  class Initializer
5
5
 
6
6
  ##
7
- # Loads the HireFire extension in to Delayed Job and
8
- # extends the Delayed Job "jobs:work" rake task command
7
+ # Loads the HireFire extension in to the loaded worker library and
8
+ # extends that library by injecting HireFire hooks in the proper locations.
9
+ #
10
+ # Currently it supports:
11
+ # - Delayed Job
12
+ # - ActiveRecord ORM
13
+ # - Mongoid ODM
14
+ # - Resque
15
+ # - Redis
16
+ #
17
+ # @note
18
+ # Either the Delayed Job, or the Resque worker library must be
19
+ # loaded BEFORE HireFire initializes, otherwise it'll be unable
20
+ # to detect the proper library and it will not work.
9
21
  #
10
22
  # @return [nil]
11
23
  def self.initialize!
24
+
12
25
  ##
13
- # If DelayedJob is using ActiveRecord, then include
14
- # HireFire::Environment in to the ActiveRecord Delayed Job Backend
15
- if defined?(Delayed::Backend::ActiveRecord::Job)
16
- Delayed::Backend::ActiveRecord::Job.
17
- send(:include, HireFire::Environment).
18
- send(:include, HireFire::Backend)
26
+ # Initialize Delayed::Job extensions if Delayed::Job is found
27
+ if defined?(::Delayed::Job)
28
+ ##
29
+ # If DelayedJob is using ActiveRecord, then include
30
+ # HireFire::Environment in to the ActiveRecord Delayed Job Backend
31
+ if defined?(::Delayed::Backend::ActiveRecord::Job)
32
+ ::Delayed::Backend::ActiveRecord::Job.
33
+ send(:include, HireFire::Environment).
34
+ send(:include, HireFire::Backend)
35
+ end
36
+
37
+ ##
38
+ # If DelayedJob is using Mongoid, then include
39
+ # HireFire::Environment in to the Mongoid Delayed Job Backend
40
+ if defined?(::Delayed::Backend::Mongoid::Job)
41
+ ::Delayed::Backend::Mongoid::Job.
42
+ send(:include, HireFire::Environment).
43
+ send(:include, HireFire::Backend)
44
+ end
45
+
46
+ ##
47
+ # Load Delayed Job extensions, this will patch Delayed::Worker
48
+ # to implement the necessary hooks to invoke HireFire from
49
+ require File.join(HireFire::WORKERS_PATH, 'delayed_job')
19
50
  end
20
51
 
21
52
  ##
22
- # If DelayedJob is using Mongoid, then include
23
- # HireFire::Environment in to the Mongoid Delayed Job Backend
24
- if defined?(Delayed::Backend::Mongoid::Job)
25
- Delayed::Backend::Mongoid::Job.
53
+ # Initialize Resque extensions if Resque is found
54
+ if defined?(::Resque)
55
+
56
+ ##
57
+ # Include the HireFire::Environment which will add an instance
58
+ # of HireFire::Environment::(Heroku|Local|Noop) to the Resque::Job.environment class method
59
+ #
60
+ # Extend the Resque::Job class with the Resque::Job.jobs class method
61
+ ::Resque::Job.
26
62
  send(:include, HireFire::Environment).
27
- send(:include, HireFire::Backend)
28
- end
63
+ send(:extend, HireFire::Backend::Resque::Redis)
29
64
 
30
- ##
31
- # Load Delayed Job extension, this is the start
32
- # method that gets invoked when running "rake jobs:work"
33
- require File.dirname(__FILE__) + '/delayed_job_extension'
65
+ ##
66
+ # Load Resque extensions, this will patch Resque, Resque::Job and Resque::Worker
67
+ # to implement the necessary hooks to invoke HireFire from
68
+ require File.join(HireFire::WORKERS_PATH, 'resque')
69
+ end
34
70
  end
35
71
 
36
72
  end
@@ -4,11 +4,37 @@ module HireFire
4
4
  class Railtie < Rails::Railtie
5
5
 
6
6
  ##
7
- # Initializes HireFire for Delayed Job when
7
+ # Initializes HireFire for either Delayed Job or Resque when
8
8
  # the Ruby on Rails web framework is done loading
9
+ #
10
+ # @note
11
+ # Either the Delayed Job, or the Resque worker library must be
12
+ # loaded BEFORE HireFire initializes, otherwise it'll be unable
13
+ # to detect the proper library and it will not work.
9
14
  initializer :after_initialize do
10
15
  HireFire::Initializer.initialize!
11
16
  end
12
17
 
18
+ ##
19
+ # Adds additional rake tasks to the Ruby on Rails environment
20
+ #
21
+ # @note
22
+ # In order for Resque to run on Heroku, it must have the 'rake jobs:work'
23
+ # rake task since that's what Heroku uses to start workers. When using
24
+ # Ruby on Rails automatically add the necessary default rake task for the user
25
+ #
26
+ # @note
27
+ # Delayed Job already has 'rake jobs:work' built in.
28
+ #
29
+ rake_tasks do
30
+
31
+ ##
32
+ # If Resque is loaded, then we load the Resque rake task
33
+ # that'll allow Heroku to start up Resque as a worker
34
+ if defined?(::Resque)
35
+ require File.join(WORKERS_PATH, 'resque', 'tasks.rb')
36
+ end
37
+ end
38
+
13
39
  end
14
40
  end
@@ -6,7 +6,7 @@ module HireFire
6
6
  ##
7
7
  # @return [String] the current version of the HireFire gem
8
8
  def self.current
9
- '0.1.0'
9
+ '0.1.1'
10
10
  end
11
11
 
12
12
  end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Load the "HireFire modified" Delayed::Worker class
5
+ require File.dirname(__FILE__) + '/delayed_job/worker'
@@ -1,5 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
+ ##
4
+ # HireFire
5
+ # This is a HireFire modified version of
6
+ # the official Delayed::Worker class
3
7
  module Delayed
4
8
  class Worker
5
9
 
@@ -42,10 +46,14 @@ module Delayed
42
46
  end
43
47
 
44
48
  ##
45
- # If there are no jobs currently queued,
46
- # and the worker is still running, it'll kill itself
49
+ # HireFire Hook
50
+ # After the last job in the queue finishes processing, Delayed::Job.new.jobs (queued.jobs)
51
+ # will return 0. This means that there aren't any more jobs to process for any of the workers.
52
+ # If this is the case it'll command the current environment to fire all the hired workers
53
+ # and then immediately break out of this infinite loop.
47
54
  if queued.jobs == 0
48
55
  Delayed::Job.environment.fire
56
+ break
49
57
  end
50
58
 
51
59
  break if $exit
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Load the "HireFire modified"
5
+ # Resque::Job and Resque::Worker classes
6
+ require File.dirname(__FILE__) + '/resque/job'
7
+ require File.dirname(__FILE__) + '/resque/worker'
8
+
9
+ ##
10
+ # HireFire
11
+ # This is a HireFire modified version of
12
+ # the official Resque module
13
+ module ::Resque
14
+ def self.enqueue(klass, *args)
15
+ Job.create(queue_from_class(klass), klass, *args)
16
+
17
+ ##
18
+ # HireFire Hook
19
+ # After a new job gets queued, we command the current environment
20
+ # to calculate the amount of workers we need to process the jobs
21
+ # that are currently queued, and hire them accordingly.
22
+ if ::Resque.info[:working].to_i == 0 \
23
+ or ::Resque.info[:jobs] == 1
24
+ ::Resque::Job.environment.hire
25
+ end
26
+
27
+ Plugin.after_enqueue_hooks(klass).each do |hook|
28
+ klass.send(hook, *args)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # HireFire
5
+ # This is a HireFire modified version of
6
+ # the official Resque::Job class
7
+ module ::Resque
8
+ class Job
9
+ def perform
10
+ job = payload_class
11
+ job_args = args || []
12
+ job_was_performed = false
13
+
14
+ before_hooks = Plugin.before_hooks(job)
15
+ around_hooks = Plugin.around_hooks(job)
16
+ after_hooks = Plugin.after_hooks(job)
17
+ failure_hooks = Plugin.failure_hooks(job)
18
+
19
+ begin
20
+ begin
21
+ before_hooks.each do |hook|
22
+ job.send(hook, *job_args)
23
+ end
24
+ rescue DontPerform
25
+ return false
26
+ end
27
+
28
+ if around_hooks.empty?
29
+ job.perform(*job_args)
30
+ job_was_performed = true
31
+ else
32
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
33
+ if last_hook
34
+ lambda do
35
+ job.send(hook, *job_args) { last_hook.call }
36
+ end
37
+ else
38
+ lambda do
39
+ job.send(hook, *job_args) do
40
+ result = job.perform(*job_args)
41
+ job_was_performed = true
42
+ result
43
+ end
44
+ end
45
+ end
46
+ end
47
+ stack.call
48
+ end
49
+
50
+ ##
51
+ # HireFire Hook
52
+ # After a job finishes processing, we invoke the #fire
53
+ # method on the environment object which will check to see whether
54
+ # we can fire all the hired workers
55
+ ::Resque::Job.environment.fire
56
+
57
+ after_hooks.each do |hook|
58
+ job.send(hook, *job_args)
59
+ end
60
+
61
+ return job_was_performed
62
+
63
+ rescue Object => e
64
+ failure_hooks.each { |hook| job.send(hook, e, *job_args) }
65
+ raise e
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Load in the official Resque rake tasks
5
+ require 'resque/tasks'
6
+
7
+ ##
8
+ # Overwrite the resque:setup rake task to first load
9
+ # in the application environment before proceeding
10
+ #
11
+ # ENV['QUEUE'] will default to '*' unless it's defined
12
+ # as an environment variable on Heroku or the Local machine
13
+ task 'resque:setup' => :environment do
14
+ ENV['QUEUE'] ||= '*'
15
+ end
16
+
17
+ ##
18
+ # This is an alias to the "resque:work" task since Heroku doesn't respond
19
+ # to "resque:work", we need to add this alias so Resque can be initialized by Heroku
20
+ desc 'Alias of "resque:work" - This is required for running on Heroku.'
21
+ task 'jobs:work' => 'resque:work'