efficiency20-delayed_job 1.8.51

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,296 @@
1
+ require 'timeout'
2
+
3
+ module Delayed
4
+
5
+ class DeserializationError < StandardError
6
+ end
7
+
8
+ # A job object that is persisted to the database.
9
+ # Contains the work object as a YAML field.
10
+ class Job < ActiveRecord::Base
11
+ @@max_attempts = 25
12
+ @@max_run_time = 4.hours
13
+
14
+ cattr_accessor :max_attempts, :max_run_time
15
+
16
+ set_table_name :delayed_jobs
17
+
18
+ # By default failed jobs are destroyed after too many attempts.
19
+ # If you want to keep them around (perhaps to inspect the reason
20
+ # for the failure), set this to false.
21
+ cattr_accessor :destroy_failed_jobs
22
+ self.destroy_failed_jobs = true
23
+
24
+ # Every worker has a unique name which by default is the pid of the process.
25
+ # There are some advantages to overriding this with something which survives worker retarts:
26
+ # Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
27
+ @@worker_name = nil
28
+
29
+ def self.worker_name
30
+ return @@worker_name unless @@worker_name.nil?
31
+ "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
32
+ end
33
+
34
+ def self.worker_name=(val)
35
+ @@worker_name = val
36
+ end
37
+
38
+ def worker_name
39
+ self.class.worker_name
40
+ end
41
+
42
+ def worker_name=(val)
43
+ @@worker_name = val
44
+ end
45
+
46
+ NextTaskSQL = '(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR (locked_by = ?)) AND failed_at IS NULL'
47
+ NextTaskOrder = 'priority DESC, run_at ASC'
48
+
49
+ ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
50
+
51
+ cattr_accessor :min_priority, :max_priority
52
+ self.min_priority = nil
53
+ self.max_priority = nil
54
+
55
+ # When a worker is exiting, make sure we don't have any locked jobs.
56
+ def self.clear_locks!
57
+ update_all("locked_by = null, locked_at = null", ["locked_by = ?", worker_name])
58
+ end
59
+
60
+ def failed?
61
+ failed_at
62
+ end
63
+ alias_method :failed, :failed?
64
+
65
+ def payload_object
66
+ @payload_object ||= deserialize(self['handler'])
67
+ end
68
+
69
+ def name
70
+ @name ||= begin
71
+ payload = payload_object
72
+ if payload.respond_to?(:display_name)
73
+ payload.display_name
74
+ else
75
+ payload.class.name
76
+ end
77
+ end
78
+ end
79
+
80
+ def payload_object=(object)
81
+ self['handler'] = object.to_yaml
82
+ end
83
+
84
+ # Reschedule the job in the future (when a job fails).
85
+ # Uses an exponential scale depending on the number of failed attempts.
86
+ def reschedule(message, backtrace = [], time = nil)
87
+ self.last_error = message + "\n" + backtrace.join("\n")
88
+
89
+ if (self.attempts += 1) < max_attempts
90
+ time ||= Job.db_time_now + (attempts ** 4) + 5
91
+
92
+ self.run_at = time
93
+ self.unlock
94
+ save!
95
+ else
96
+ logger.info "* [JOB] PERMANENTLY removing #{self.name} because of #{attempts} consecutive failures."
97
+ destroy_failed_jobs ? destroy : update_attribute(:failed_at, Delayed::Job.db_time_now)
98
+ end
99
+ end
100
+
101
+
102
+ # Try to run one job. Returns true/false (work done/work failed) or nil if job can't be locked.
103
+ def run_with_lock(max_run_time, worker_name)
104
+ logger.info "* [JOB] acquiring lock on #{name}"
105
+ unless lock_exclusively!(max_run_time, worker_name)
106
+ # We did not get the lock, some other worker process must have
107
+ logger.warn "* [JOB] failed to acquire exclusive lock for #{name}"
108
+ return nil # no work done
109
+ end
110
+
111
+ begin
112
+ runtime = Benchmark.realtime do
113
+ Timeout.timeout(max_run_time.to_i) { invoke_job }
114
+ destroy
115
+ end
116
+ # TODO: warn if runtime > max_run_time ?
117
+ logger.info "* [JOB] #{name} completed after %.4f" % runtime
118
+ return true # did work
119
+ rescue Exception => e
120
+ reschedule e.message, e.backtrace
121
+ log_exception(e)
122
+ return false # work failed
123
+ end
124
+ end
125
+
126
+ # Add a job to the queue
127
+ def self.enqueue(*args, &block)
128
+ object = block_given? ? EvaledJob.new(&block) : args.shift
129
+
130
+ unless object.respond_to?(:perform) || block_given?
131
+ raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
132
+ end
133
+
134
+ priority = args.first || 0
135
+ run_at = args[1]
136
+
137
+ Job.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
138
+ end
139
+
140
+ # Find a few candidate jobs to run (in case some immediately get locked by others).
141
+ def self.find_available(limit = 5, max_run_time = max_run_time)
142
+
143
+ time_now = db_time_now
144
+
145
+ sql = NextTaskSQL.dup
146
+
147
+ conditions = [time_now, time_now - max_run_time, worker_name]
148
+
149
+ if self.min_priority
150
+ sql << ' AND (priority >= ?)'
151
+ conditions << min_priority
152
+ end
153
+
154
+ if self.max_priority
155
+ sql << ' AND (priority <= ?)'
156
+ conditions << max_priority
157
+ end
158
+
159
+ conditions.unshift(sql)
160
+
161
+ ActiveRecord::Base.silence do
162
+ find(:all, :conditions => conditions, :order => NextTaskOrder, :limit => limit)
163
+ end
164
+ end
165
+
166
+ # Run the next job we can get an exclusive lock on.
167
+ # If no jobs are left we return nil
168
+ def self.reserve_and_run_one_job(max_run_time = max_run_time)
169
+
170
+ # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
171
+ # this leads to a more even distribution of jobs across the worker processes
172
+ find_available(5, max_run_time).each do |job|
173
+ t = job.run_with_lock(max_run_time, worker_name)
174
+ return t unless t == nil # return if we did work (good or bad)
175
+ end
176
+
177
+ nil # we didn't do any work, all 5 were not lockable
178
+ end
179
+
180
+ # Lock this job for this worker.
181
+ # Returns true if we have the lock, false otherwise.
182
+ def lock_exclusively!(max_run_time, worker = worker_name)
183
+ now = self.class.db_time_now
184
+ affected_rows = if locked_by != worker
185
+ # We don't own this job so we will update the locked_by name and the locked_at
186
+ self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and (locked_at is null or locked_at < ?) and (run_at <= ?)", id, (now - max_run_time.to_i), now])
187
+ else
188
+ # We already own this job, this may happen if the job queue crashes.
189
+ # Simply resume and update the locked_at
190
+ self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
191
+ end
192
+ if affected_rows == 1
193
+ self.locked_at = now
194
+ self.locked_by = worker
195
+ return true
196
+ else
197
+ return false
198
+ end
199
+ end
200
+
201
+ # Unlock this job (note: not saved to DB)
202
+ def unlock
203
+ self.locked_at = nil
204
+ self.locked_by = nil
205
+ end
206
+
207
+ # This is a good hook if you need to report job processing errors in additional or different ways
208
+ def log_exception(error)
209
+ logger.error "* [JOB] #{name} failed with #{error.class.name}: #{error.message} - #{attempts} failed attempts"
210
+ logger.error(error)
211
+ end
212
+
213
+ # Do num jobs and return stats on success/failure.
214
+ # Exit early if interrupted.
215
+ def self.work_off(num = 100)
216
+ success, failure = 0, 0
217
+
218
+ num.times do
219
+ case self.reserve_and_run_one_job
220
+ when true
221
+ success += 1
222
+ when false
223
+ failure += 1
224
+ else
225
+ break # leave if no work could be done
226
+ end
227
+ break if $exit # leave if we're exiting
228
+ end
229
+
230
+ return [success, failure]
231
+ end
232
+
233
+ # Moved into its own method so that new_relic can trace it.
234
+ def invoke_job
235
+ payload_object.perform
236
+ end
237
+
238
+ private
239
+
240
+ def deserialize(source)
241
+ handler = YAML.load(source) rescue nil
242
+
243
+ unless handler.respond_to?(:perform)
244
+ if handler.nil? && source =~ ParseObjectFromYaml
245
+ handler_class = $1
246
+ end
247
+ attempt_to_load(handler_class || handler.class)
248
+ handler = YAML.load(source)
249
+ end
250
+
251
+ return handler if handler.respond_to?(:perform)
252
+
253
+ raise DeserializationError,
254
+ 'Job failed to load: Unknown handler. Try to manually require the appropriate file.'
255
+ rescue TypeError, LoadError, NameError => e
256
+ raise DeserializationError,
257
+ "Job failed to load: #{e.message}. Try to manually require the required file."
258
+ end
259
+
260
+ # Constantize the object so that ActiveSupport can attempt
261
+ # its auto loading magic. Will raise LoadError if not successful.
262
+ def attempt_to_load(klass)
263
+ klass.constantize
264
+ end
265
+
266
+ # Get the current time (GMT or local depending on DB)
267
+ # Note: This does not ping the DB to get the time, so all your clients
268
+ # must have syncronized clocks.
269
+ def self.db_time_now
270
+ if (defined? Time.zone) && Time.zone
271
+ Time.zone.now
272
+ elsif ActiveRecord::Base.default_timezone == :utc
273
+ Time.now.utc
274
+ else
275
+ Time.now
276
+ end
277
+ end
278
+
279
+ protected
280
+
281
+ def before_save
282
+ self.run_at ||= self.class.db_time_now
283
+ end
284
+
285
+ end
286
+
287
+ class EvaledJob
288
+ def initialize
289
+ @job = yield
290
+ end
291
+
292
+ def perform
293
+ eval(@job)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,22 @@
1
+ module Delayed
2
+ module MessageSending
3
+ def send_later(method, *args)
4
+ Delayed::Job.enqueue Delayed::PerformableMethod.new(self, method.to_sym, args)
5
+ end
6
+
7
+ def send_at(time, method, *args)
8
+ Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args), 0, time)
9
+ end
10
+
11
+ module ClassMethods
12
+ def handle_asynchronously(method)
13
+ aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
14
+ with_method, without_method = "#{aliased_method}_with_send_later#{punctuation}", "#{aliased_method}_without_send_later#{punctuation}"
15
+ define_method(with_method) do |*args|
16
+ send_later(without_method, *args)
17
+ end
18
+ alias_method_chain method, :send_later
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ module Delayed
2
+ class PerformableMethod < Struct.new(:object, :method, :args)
3
+ CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
4
+ AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
5
+
6
+ def initialize(object, method, args)
7
+ raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method)
8
+
9
+ self.object = dump(object)
10
+ self.args = args.map { |a| dump(a) }
11
+ self.method = method.to_sym
12
+ end
13
+
14
+ def display_name
15
+ case self.object
16
+ when CLASS_STRING_FORMAT then "#{$1}.#{method}"
17
+ when AR_STRING_FORMAT then "#{$1}##{method}"
18
+ else "Unknown##{method}"
19
+ end
20
+ end
21
+
22
+ def perform
23
+ load(object).send(method, *args.map{|a| load(a)})
24
+ rescue ActiveRecord::RecordNotFound
25
+ # We cannot do anything about objects which were deleted in the meantime
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def load(arg)
32
+ case arg
33
+ when CLASS_STRING_FORMAT then $1.constantize
34
+ when AR_STRING_FORMAT then $1.constantize.find($2)
35
+ else arg
36
+ end
37
+ end
38
+
39
+ def dump(arg)
40
+ case arg
41
+ when Class then class_to_string(arg)
42
+ when ActiveRecord::Base then ar_to_string(arg)
43
+ else arg
44
+ end
45
+ end
46
+
47
+ def ar_to_string(obj)
48
+ "AR:#{obj.class}:#{obj.id}"
49
+ end
50
+
51
+ def class_to_string(obj)
52
+ "CLASS:#{obj.name}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,31 @@
1
+ # Capistrano Recipes for managing delayed_job
2
+ #
3
+ # Add these callbacks to have the delayed_job process restart when the server
4
+ # is restarted:
5
+ #
6
+ # after "deploy:stop", "delayed_job:stop"
7
+ # after "deploy:start", "delayed_job:start"
8
+ # after "deploy:restart", "delayed_job:restart"
9
+
10
+ Capistrano::Configuration.instance.load do
11
+ namespace :delayed_job do
12
+ def rails_env
13
+ fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
14
+ end
15
+
16
+ desc "Stop the delayed_job process"
17
+ task :stop, :roles => :app do
18
+ run "cd #{current_path};#{rails_env} script/delayed_job stop"
19
+ end
20
+
21
+ desc "Start the delayed_job process"
22
+ task :start, :roles => :app do
23
+ run "cd #{current_path};#{rails_env} script/delayed_job start"
24
+ end
25
+
26
+ desc "Restart the delayed_job process"
27
+ task :restart, :roles => :app do
28
+ run "cd #{current_path};#{rails_env} script/delayed_job restart"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # Re-definitions are appended to existing tasks
2
+ task :environment
3
+ task :merb_env
4
+
5
+ namespace :jobs do
6
+ desc "Clear the delayed_job queue."
7
+ task :clear => [:merb_env, :environment] do
8
+ Delayed::Job.delete_all
9
+ end
10
+
11
+ desc "Start a delayed_job worker."
12
+ task :work => [:merb_env, :environment] do
13
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY']).start
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ module Delayed
2
+ class Worker
3
+ @@sleep_delay = 5
4
+
5
+ cattr_accessor :sleep_delay
6
+
7
+ cattr_accessor :logger
8
+ self.logger = if defined?(Merb::Logger)
9
+ Merb.logger
10
+ elsif defined?(RAILS_DEFAULT_LOGGER)
11
+ RAILS_DEFAULT_LOGGER
12
+ end
13
+
14
+ # name_prefix is ignored if name is set directly
15
+ attr_accessor :name_prefix
16
+
17
+ def job_max_run_time
18
+ Delayed::Job.max_run_time
19
+ end
20
+
21
+ # Every worker has a unique name which by default is the pid of the process.
22
+ # There are some advantages to overriding this with something which survives worker retarts:
23
+ # Workers can safely resume working on tasks which are locked by themselves. The worker will assume that it crashed before.
24
+ def name
25
+ return @name unless @name.nil?
26
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
27
+ end
28
+
29
+ # Sets the name of the worker.
30
+ # Setting the name to nil will reset the default worker name
31
+ def name=(val)
32
+ @name = val
33
+ end
34
+
35
+ def initialize(options={})
36
+ @quiet = options[:quiet]
37
+ Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
38
+ Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
39
+ end
40
+
41
+ def start
42
+ say "*** Starting job worker #{Delayed::Job.worker_name}"
43
+
44
+ trap('TERM') { say 'Exiting...'; $exit = true }
45
+ trap('INT') { say 'Exiting...'; $exit = true }
46
+
47
+ loop do
48
+ result = nil
49
+
50
+ realtime = Benchmark.realtime do
51
+ result = Delayed::Job.work_off
52
+ end
53
+
54
+ count = result.sum
55
+
56
+ break if $exit
57
+
58
+ if count.zero?
59
+ sleep(@@sleep_delay)
60
+ else
61
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
62
+ end
63
+
64
+ break if $exit
65
+ end
66
+
67
+ ensure
68
+ Delayed::Job.clear_locks!
69
+ end
70
+
71
+ def say(text)
72
+ puts text unless @quiet
73
+ logger.info text if logger
74
+ end
75
+
76
+ end
77
+ end