efficiency20-delayed_job 1.8.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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