delayed_job_hooked 2.1.5

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,112 @@
1
+ require 'rubygems'
2
+ require 'daemons'
3
+ require 'optparse'
4
+
5
+ module Delayed
6
+ class Command
7
+ attr_accessor :worker_count
8
+
9
+ def initialize(args)
10
+ @files_to_reopen = []
11
+ @options = {
12
+ :quiet => true,
13
+ :pid_dir => "#{Rails.root}/tmp/pids"
14
+ }
15
+
16
+ @worker_count = 1
17
+ @monitor = false
18
+
19
+ opts = OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
21
+
22
+ opts.on('-h', '--help', 'Show this message') do
23
+ puts opts
24
+ exit 1
25
+ end
26
+ opts.on('-e', '--environment=NAME', 'Specifies the environment to run this delayed jobs under (test/development/production).') do |e|
27
+ STDERR.puts "The -e/--environment option has been deprecated and has no effect. Use RAILS_ENV and see http://github.com/collectiveidea/delayed_job/issues/#issue/7"
28
+ end
29
+ opts.on('--min-priority N', 'Minimum priority of jobs to run.') do |n|
30
+ @options[:min_priority] = n
31
+ end
32
+ opts.on('--max-priority N', 'Maximum priority of jobs to run.') do |n|
33
+ @options[:max_priority] = n
34
+ end
35
+ opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") do |worker_count|
36
+ @worker_count = worker_count.to_i rescue 1
37
+ end
38
+ opts.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
39
+ @options[:pid_dir] = dir
40
+ end
41
+ opts.on('-i', '--identifier=n', 'A numeric identifier for the worker.') do |n|
42
+ @options[:identifier] = n
43
+ end
44
+ opts.on('-m', '--monitor', 'Start monitor process.') do
45
+ @monitor = true
46
+ end
47
+ opts.on('--sleep-delay N', "Amount of time to sleep when no jobs are found") do |n|
48
+ @options[:sleep_delay] = n
49
+ end
50
+ opts.on('-p', '--prefix NAME', "String to be prefixed to worker process names") do |prefix|
51
+ @options[:prefix] = prefix
52
+ end
53
+ end
54
+ @args = opts.parse!(args)
55
+ end
56
+
57
+ def daemonize
58
+ Delayed::Worker.backend.before_fork
59
+
60
+ ObjectSpace.each_object(File) do |file|
61
+ @files_to_reopen << file unless file.closed?
62
+ end
63
+
64
+ dir = @options[:pid_dir]
65
+ Dir.mkdir(dir) unless File.exists?(dir)
66
+
67
+ if @worker_count > 1 && @options[:identifier]
68
+ raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
69
+ elsif @worker_count == 1 && @options[:identifier]
70
+ process_name = "delayed_job.#{@options[:identifier]}"
71
+ run_process(process_name, dir)
72
+ else
73
+ worker_count.times do |worker_index|
74
+ process_name = worker_count == 1 ? "delayed_job" : "delayed_job.#{worker_index}"
75
+ run_process(process_name, dir)
76
+ end
77
+ end
78
+ end
79
+
80
+ def run_process(process_name, dir)
81
+ Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
82
+ $0 = File.join(@options[:prefix], process_name) if @options[:prefix]
83
+ run process_name
84
+ end
85
+ end
86
+
87
+ def run(worker_name = nil)
88
+ Dir.chdir(Rails.root)
89
+
90
+ # Re-open file handles
91
+ @files_to_reopen.each do |file|
92
+ begin
93
+ file.reopen file.path, "a+"
94
+ file.sync = true
95
+ rescue ::Exception
96
+ end
97
+ end
98
+
99
+ Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
100
+ Delayed::Worker.backend.after_fork
101
+
102
+ worker = Delayed::Worker.new(@options)
103
+ worker.name_prefix = "#{worker_name} "
104
+ worker.start
105
+ rescue => e
106
+ Rails.logger.fatal e
107
+ STDERR.puts e.message
108
+ exit 1
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,4 @@
1
+ module Delayed
2
+ class DeserializationError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/basic_object'
2
+ require 'active_support/core_ext/module/aliasing'
3
+
4
+ module Delayed
5
+ class DelayProxy < ActiveSupport::BasicObject
6
+ def initialize(payload_class, target, options)
7
+ @payload_class = payload_class
8
+ @target = target
9
+ @options = options
10
+ end
11
+
12
+ def method_missing(method, *args)
13
+ Job.enqueue({:payload_object => @payload_class.new(@target, method.to_sym, args)}.merge(@options))
14
+ end
15
+ end
16
+
17
+ module MessageSending
18
+ def delay(options = {})
19
+ DelayProxy.new(PerformableMethod, self, options)
20
+ end
21
+ alias __delay__ delay
22
+
23
+ def send_later(method, *args)
24
+ warn "[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method"
25
+ __delay__.__send__(method, *args)
26
+ end
27
+
28
+ def send_at(time, method, *args)
29
+ warn "[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method"
30
+ __delay__(:run_at => time).__send__(method, *args)
31
+ end
32
+
33
+ module ClassMethods
34
+ def handle_asynchronously(method, opts = {})
35
+ aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
36
+ with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
37
+ define_method(with_method) do |*args|
38
+ curr_opts = opts.clone
39
+ curr_opts.each_key do |key|
40
+ if (val = curr_opts[key]).is_a?(Proc)
41
+ curr_opts[key] = if val.arity == 1
42
+ val.call(self)
43
+ else
44
+ val.call
45
+ end
46
+ end
47
+ end
48
+ delay(curr_opts).__send__(without_method, *args)
49
+ end
50
+ alias_method_chain method, :delay
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ require 'mail'
2
+
3
+ module Delayed
4
+ class PerformableMailer < PerformableMethod
5
+ def perform
6
+ object.send(method_name, *args).deliver
7
+ end
8
+ end
9
+
10
+ module DelayMail
11
+ def delay(options = {})
12
+ DelayProxy.new(PerformableMailer, self, options)
13
+ end
14
+ end
15
+ end
16
+
17
+ Mail::Message.class_eval do
18
+ def delay(*args)
19
+ raise RuntimeError, "Use MyMailer.delay.mailer_action(args) to delay sending of emails."
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Delayed
4
+ class PerformableMethod < Struct.new(:object, :method_name, :args)
5
+ delegate :method, :to => :object
6
+
7
+ def initialize(object, method_name, args)
8
+ raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
9
+
10
+ self.object = object
11
+ self.args = args
12
+ self.method_name = method_name.to_sym
13
+ end
14
+
15
+ def display_name
16
+ "#{object.class}##{method_name}"
17
+ end
18
+
19
+ def perform
20
+ object.send(method_name, *args) if object
21
+ end
22
+
23
+ def method_missing(symbol, *args)
24
+ object.send(symbol, *args)
25
+ end
26
+
27
+ def respond_to?(symbol, include_private=false)
28
+ super || object.respond_to?(symbol, include_private)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ require 'delayed_job'
2
+ require 'rails'
3
+
4
+ module Delayed
5
+ class Railtie < Rails::Railtie
6
+ initializer :after_initialize do
7
+ Delayed::Worker.guess_backend
8
+
9
+ ActiveSupport.on_load(:action_mailer) do
10
+ ActionMailer::Base.send(:extend, Delayed::DelayMail)
11
+ end
12
+ end
13
+
14
+ rake_tasks do
15
+ load 'delayed/tasks.rb'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,50 @@
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
+ # If you want to use command line options, for example to start multiple workers,
11
+ # define a Capistrano variable delayed_job_args:
12
+ #
13
+ # set :delayed_job_args, "-n 2"
14
+ #
15
+ # If you've got delayed_job workers running on a servers, you can also specify
16
+ # which servers have delayed_job running and should be restarted after deploy.
17
+ #
18
+ # set :delayed_job_server_role, :worker
19
+ #
20
+
21
+ Capistrano::Configuration.instance.load do
22
+ namespace :delayed_job do
23
+ def rails_env
24
+ fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
25
+ end
26
+
27
+ def args
28
+ fetch(:delayed_job_args, "")
29
+ end
30
+
31
+ def roles
32
+ fetch(:delayed_job_server_role, :app)
33
+ end
34
+
35
+ desc "Stop the delayed_job process"
36
+ task :stop, :roles => lambda { roles } do
37
+ run "cd #{current_path};#{rails_env} script/delayed_job stop"
38
+ end
39
+
40
+ desc "Start the delayed_job process"
41
+ task :start, :roles => lambda { roles } do
42
+ run "cd #{current_path};#{rails_env} script/delayed_job start #{args}"
43
+ end
44
+
45
+ desc "Restart the delayed_job process"
46
+ task :restart, :roles => lambda { roles } do
47
+ run "cd #{current_path};#{rails_env} script/delayed_job restart #{args}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ class ActiveRecord::Base
2
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
3
+
4
+ def self.yaml_new(klass, tag, val)
5
+ klass.find(val['attributes']['id'])
6
+ rescue ActiveRecord::RecordNotFound
7
+ raise Delayed::DeserializationError
8
+ end
9
+
10
+ def to_yaml_properties
11
+ ['@attributes']
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ namespace :jobs do
2
+ desc "Clear the delayed_job queue."
3
+ task :clear => :environment do
4
+ Delayed::Job.delete_all
5
+ end
6
+
7
+ desc "Start a delayed_job worker."
8
+ task :work => :environment do
9
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :quiet => false).start
10
+ end
11
+ end
@@ -0,0 +1,182 @@
1
+ require 'timeout'
2
+ require 'active_support/core_ext/numeric/time'
3
+ require 'active_support/core_ext/class/attribute_accessors'
4
+ require 'active_support/core_ext/kernel'
5
+ require 'logger'
6
+
7
+ module Delayed
8
+ class Worker
9
+ cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time, :default_priority, :sleep_delay, :logger, :delay_jobs
10
+ self.sleep_delay = 5
11
+ self.max_attempts = 25
12
+ self.max_run_time = 4.hours
13
+ self.default_priority = 0
14
+ self.delay_jobs = true
15
+
16
+ # By default failed jobs are destroyed after too many attempts. If you want to keep them around
17
+ # (perhaps to inspect the reason for the failure), set this to false.
18
+ cattr_accessor :destroy_failed_jobs
19
+ self.destroy_failed_jobs = true
20
+
21
+ self.logger = if defined?(Rails)
22
+ Rails.logger
23
+ elsif defined?(RAILS_DEFAULT_LOGGER)
24
+ RAILS_DEFAULT_LOGGER
25
+ end
26
+
27
+ # name_prefix is ignored if name is set directly
28
+ attr_accessor :name_prefix
29
+
30
+ cattr_reader :backend
31
+
32
+ def self.backend=(backend)
33
+ if backend.is_a? Symbol
34
+ require "delayed/serialization/#{backend}"
35
+ require "delayed/backend/#{backend}"
36
+ backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
37
+ end
38
+ @@backend = backend
39
+ silence_warnings { ::Delayed.const_set(:Job, backend) }
40
+ end
41
+
42
+ def self.guess_backend
43
+ self.backend ||= :active_record if defined?(ActiveRecord)
44
+ end
45
+
46
+ def initialize(options={})
47
+ @quiet = options.has_key?(:quiet) ? options[:quiet] : true
48
+ self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
49
+ self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
50
+ self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
51
+ end
52
+
53
+ # Every worker has a unique name which by default is the pid of the process. There are some
54
+ # advantages to overriding this with something which survives worker retarts: Workers can#
55
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
56
+ # it crashed before.
57
+ def name
58
+ return @name unless @name.nil?
59
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
60
+ end
61
+
62
+ # Sets the name of the worker.
63
+ # Setting the name to nil will reset the default worker name
64
+ def name=(val)
65
+ @name = val
66
+ end
67
+
68
+ def start
69
+ say "Starting job worker"
70
+
71
+ trap('TERM') { say 'Exiting...'; $exit = true }
72
+ trap('INT') { say 'Exiting...'; $exit = true }
73
+
74
+ loop do
75
+ result = nil
76
+
77
+ realtime = Benchmark.realtime do
78
+ result = work_off
79
+ end
80
+
81
+ count = result.sum
82
+
83
+ break if $exit
84
+
85
+ if count.zero?
86
+ sleep(self.class.sleep_delay)
87
+ else
88
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
89
+ end
90
+
91
+ break if $exit
92
+ end
93
+
94
+ ensure
95
+ Delayed::Job.clear_locks!(name)
96
+ end
97
+
98
+ # Do num jobs and return stats on success/failure.
99
+ # Exit early if interrupted.
100
+ def work_off(num = 100)
101
+ success, failure = 0, 0
102
+
103
+ num.times do
104
+ case reserve_and_run_one_job
105
+ when true
106
+ success += 1
107
+ when false
108
+ failure += 1
109
+ else
110
+ break # leave if no work could be done
111
+ end
112
+ break if $exit # leave if we're exiting
113
+ end
114
+
115
+ return [success, failure]
116
+ end
117
+
118
+ def run(job)
119
+ runtime = Benchmark.realtime do
120
+ Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
121
+ job.destroy
122
+ end
123
+ say "#{job.name} completed after %.4f" % runtime
124
+ job.hook(:completed)
125
+ return true # did work
126
+ rescue DeserializationError => error
127
+ job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
128
+ failed(job)
129
+ rescue Exception => error
130
+ handle_failed_job(job, error)
131
+ return false # work failed
132
+ end
133
+
134
+ # Reschedule the job in the future (when a job fails).
135
+ # Uses an exponential scale depending on the number of failed attempts.
136
+ def reschedule(job, time = nil)
137
+ if (job.attempts += 1) < max_attempts(job)
138
+ time ||= job.reschedule_at
139
+ job.run_at = time
140
+ job.unlock
141
+ job.save!
142
+ else
143
+ say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
144
+ failed(job)
145
+ end
146
+ end
147
+
148
+ def failed(job)
149
+ job.hook(:failure)
150
+ if job.respond_to?(:on_permanent_failure)
151
+ warn "[DEPRECATION] The #on_permanent_failure hook has been renamed to #failure."
152
+ end
153
+ self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
154
+ end
155
+
156
+ def say(text, level = Logger::INFO)
157
+ text = "[Worker(#{name})] #{text}"
158
+ puts text unless @quiet
159
+ logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
160
+ end
161
+
162
+ def max_attempts(job)
163
+ job.max_attempts || self.class.max_attempts
164
+ end
165
+
166
+ protected
167
+
168
+ def handle_failed_job(job, error)
169
+ job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
170
+ say "#{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
171
+ reschedule(job)
172
+ end
173
+
174
+ # Run the next job we can get an exclusive lock on.
175
+ # If no jobs are left we return nil
176
+ def reserve_and_run_one_job
177
+ job = Delayed::Job.reserve(self)
178
+ run(job) if job
179
+ end
180
+ end
181
+
182
+ end