samoli-hirefire 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module DelayedJob
6
+ module ActiveRecord
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs are excluded from the sum
11
+ #
12
+ # @return [Fixnum] the amount of pending jobs
13
+ def jobs
14
+ ::Delayed::Job.
15
+ where(:failed_at => nil).
16
+ where('run_at <= ?', Time.now).count
17
+ end
18
+
19
+ ##
20
+ # Counts the amount of jobs that are locked by a worker
21
+ #
22
+ # @return [Fixnum] the amount of (assumably working) workers
23
+ def workers
24
+ ::Delayed::Job.
25
+ where('locked_by IS NOT NULL').count
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module DelayedJob
6
+ module Mongoid
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs and jobs scheduled for the future are excluded
11
+ #
12
+ # @return [Fixnum]
13
+ def jobs
14
+ ::Delayed::Job.where(
15
+ :failed_at => nil,
16
+ :run_at.lte => Time.now
17
+ ).count
18
+ end
19
+
20
+ ##
21
+ # Counts the amount of jobs that are locked by a worker
22
+ #
23
+ # @return [Fixnum] the amount of (assumably working) workers
24
+ def workers
25
+ ::Delayed::Job.
26
+ where(:locked_by.ne => nil).count
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Backend
5
+ module Resque
6
+ module Redis
7
+
8
+ ##
9
+ # Counts the amount of queued jobs in the database,
10
+ # failed jobs and jobs scheduled for the future are excluded
11
+ #
12
+ # @return [Fixnum]
13
+ def jobs
14
+ ::Resque.info[:pending].to_i + ::Resque.info[:working].to_i
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ class Configuration
5
+
6
+ ##
7
+ # Contains the max amount of workers that are allowed to run concurrently
8
+ #
9
+ # @return [Fixnum] default: 1
10
+ attr_accessor :max_workers
11
+
12
+ ##
13
+ # Contains the min amount of workers that should always be running
14
+ #
15
+ # @return [Fixnum] default: 0
16
+ attr_accessor :min_workers
17
+
18
+ ##
19
+ # Contains the job/worker ratio which determines
20
+ # how many workers need to be running depending on
21
+ # the amount of pending jobs
22
+ #
23
+ # @return [Array] containing one or more hashes
24
+ attr_accessor :job_worker_ratio
25
+
26
+ ##
27
+ # Default is nil, in which case it'll auto-detect either :heroku or :noop,
28
+ # depending on the environment. It will never use :local, unless explicitly defined by the user.
29
+ #
30
+ # @param [Symbol, nil] environment Contains the name of the environment to run in.
31
+ # @return [Symbol, nil] default: nil
32
+ attr_accessor :environment
33
+
34
+ ##
35
+ # Instantiates a new HireFire::Configuration object
36
+ # with the default configuration. These default configurations
37
+ # may be overwritten using the HireFire.configure class method
38
+ #
39
+ # @return [HireFire::Configuration]
40
+ def initialize
41
+ @max_workers = 1
42
+ @min_workers = 0
43
+ @job_worker_ratio = [
44
+ { :jobs => 1, :workers => 1 },
45
+ { :jobs => 25, :workers => 2 },
46
+ { :jobs => 50, :workers => 3 },
47
+ { :jobs => 75, :workers => 4 },
48
+ { :jobs => 100, :workers => 5 }
49
+ ]
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Environment
5
+
6
+ ##
7
+ # This module gets included in either:
8
+ # - Delayed::Backend::ActiveRecord::Job
9
+ # - Delayed::Backend::Mongoid::Job
10
+ # - Resque::Job
11
+ #
12
+ # One of these classes will then be provided with an instance of one of the following:
13
+ # - HireFire::Environment::Heroku
14
+ # - HireFire::Environment::Local
15
+ # - HireFire::Environment::Noop
16
+ #
17
+ # This instance is stored in the Class.environment class method
18
+ #
19
+ # The Delayed Job classes receive 3 hooks:
20
+ # - hirefire_hire ( invoked when a job gets queued )
21
+ # - environment.fire ( invoked when a queued job gets destroyed )
22
+ # - environment.fire ( invoked when a queued job gets updated unless the job didn't fail )
23
+ #
24
+ # The Resque classes get their hooks injected from the HireFire::Initializer#initialize! method
25
+ #
26
+ # @param (Class) base This is the class in which this module will be included
27
+ def self.included(base)
28
+ base.send :extend, HireFire::Environment::ClassMethods
29
+
30
+ ##
31
+ # Only implement these hooks for Delayed::Job backends
32
+ if base.name =~ /Delayed::Backend::(ActiveRecord|Mongoid)::Job/
33
+ base.send :extend, HireFire::Environment::DelayedJob::ClassMethods
34
+
35
+ base.class_eval do
36
+ after_create 'self.class.hirefire_hire'
37
+ after_destroy 'self.class.environment.fire'
38
+ after_update 'self.class.environment.fire',
39
+ :unless => Proc.new { |job| job.failed_at.nil? }
40
+ end
41
+ end
42
+
43
+ Logger.message("#{ base.name } detected!")
44
+ end
45
+
46
+ ##
47
+ # Class methods that will be added to the
48
+ # Delayed::Job and Resque::Job classes
49
+ module ClassMethods
50
+
51
+ ##
52
+ # Returns the environment class method (containing an instance of the proper environment class)
53
+ # for either Delayed::Job or Resque::Job
54
+ #
55
+ # If HireFire.configuration.environment is nil (the default) then it'll
56
+ # auto-detect which environment to run in (either Heroku or Noop)
57
+ #
58
+ # If HireFire.configuration.environment isn't nil (explicitly set) then
59
+ # it'll run in the specified environment (Heroku, Local or Noop)
60
+ #
61
+ # @return [HireFire::Environment::Heroku, HireFire::Environment::Local, HireFire::Environment::Noop]
62
+ def environment
63
+ @environment ||= HireFire::Environment.const_get(
64
+ if environment = HireFire.configuration.environment
65
+ environment.to_s.camelize
66
+ else
67
+ ENV.include?('HEROKU_UPID') ? 'Heroku' : 'Noop'
68
+ end
69
+ ).new
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Delayed Job specific module
75
+ module DelayedJob
76
+ module ClassMethods
77
+
78
+ ##
79
+ # This method is an attempt to improve web-request throughput.
80
+ #
81
+ # A class method for Delayed::Job which first checks if any worker is currently
82
+ # running by checking to see if there are any jobs locked by a worker. If there aren't
83
+ # any jobs locked by a worker there is a high chance that there aren't any workers running.
84
+ # If this is the case, then we sure also invoke the 'self.class.environment.hire' method
85
+ #
86
+ # Another check is to see if there is only 1 job (which is the one that
87
+ # was just added before this callback invoked). If this is the case
88
+ # then it's very likely there aren't any workers running and we should
89
+ # invoke the 'self.class.environment.hire' method to make sure this is the case.
90
+ #
91
+ # @return [nil]
92
+ def hirefire_hire
93
+ delayed_job = ::Delayed::Job.new
94
+ if delayed_job.workers == 0 \
95
+ or delayed_job.jobs == 1
96
+ environment.hire
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,213 @@
1
+ # encoding: utf-8
2
+
3
+ module HireFire
4
+ module Environment
5
+ class Base
6
+
7
+ ##
8
+ # Include HireFire::Backend helpers
9
+ include HireFire::Backend
10
+
11
+ ##
12
+ # This method gets invoked when a new job has been queued
13
+ #
14
+ # Iterates through the default (or user-defined) job/worker ratio until
15
+ # it finds a match for the for the current situation (see example).
16
+ #
17
+ # @example
18
+ # # Say we have 40 queued jobs, and we configured our job/worker ratio like so:
19
+ #
20
+ # HireFire.configure do |config|
21
+ # config.max_workers = 5
22
+ # config.job_worker_ratio = [
23
+ # { :jobs => 1, :workers => 1 },
24
+ # { :jobs => 15, :workers => 2 },
25
+ # { :jobs => 35, :workers => 3 },
26
+ # { :jobs => 60, :workers => 4 },
27
+ # { :jobs => 80, :workers => 5 }
28
+ # ]
29
+ # end
30
+ #
31
+ # # It'll match at { :jobs => 35, :workers => 3 }, (35 jobs or more: hire 3 workers)
32
+ # # meaning that it'll ensure there are 3 workers running.
33
+ #
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
+ #
48
+ # # If there were more than 3 workers running (say, 4 or 5), it will NOT reduce
49
+ # # the number. This is because when you reduce the number of workers, you cannot
50
+ # # tell which worker Heroku will shut down, meaning you might interrupt a worker
51
+ # # that's currently working, causing the job to fail. Also, consider the fact that
52
+ # # there are, for example, 35 jobs still to be picked up, so the more workers,
53
+ # # the faster it processes. You aren't even paying more because it doesn't matter whether
54
+ # # you have 1 worker, or 5 workers processing jobs, because workers are pro-rated to the second.
55
+ # # So basically 5 workers would cost 5 times more, but will also process 5 times faster.
56
+ #
57
+ # # Once all jobs finished processing (e.g. Delayed::Job.jobs == 0), HireFire will invoke a signal
58
+ # # which will set the workers back to 0 and shuts down all the workers simultaneously.
59
+ #
60
+ # @return [nil]
61
+ def hire
62
+ jobs_count = jobs
63
+ workers_count = workers
64
+
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
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|
119
+
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
135
+ end
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
156
+ end
157
+
158
+ ##
159
+ # This method gets invoked when a job is either "destroyed"
160
+ # or "updated, unless the job didn't fail"
161
+ #
162
+ # If there are workers active, but there are no more pending jobs,
163
+ # then fire all the workers or set to the minimum_workers
164
+ #
165
+ # @return [nil]
166
+ def fire
167
+ if jobs == 0 and workers > min_workers
168
+ Logger.message("All queued jobs have been processed. " + (min_workers > 0 ? "Setting workers to #{min_workers}." : "Firing all workers."))
169
+ workers(min_workers)
170
+ end
171
+ end
172
+
173
+ private
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
+
184
+ ##
185
+ # Wrapper method for HireFire.configuration
186
+ # Returns the max amount of workers that may run concurrently
187
+ #
188
+ # @return [Fixnum] the max amount of workers that are allowed to run concurrently
189
+ def max_workers
190
+ HireFire.configuration.max_workers
191
+ end
192
+
193
+ ##
194
+ # Wrapper method for HireFire.configuration
195
+ # Returns the min amount of workers that should always be running
196
+ #
197
+ # @return [Fixnum] the min amount of workers that should always be running
198
+ def min_workers
199
+ HireFire.configuration.min_workers
200
+ end
201
+
202
+ ##
203
+ # Wrapper method for HireFire.configuration
204
+ # Returns the job/worker ratio array (in reversed order)
205
+ #
206
+ # @return [Array] the array of hashes containing the job/worker ratio
207
+ def ratio
208
+ HireFire.configuration.job_worker_ratio
209
+ end
210
+
211
+ end
212
+ end
213
+ end