inst-jobs 0.11.0
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.
- checksums.yaml +7 -0
- data/bin/inst_job +4 -0
- data/db/migrate/20101216224513_create_delayed_jobs.rb +40 -0
- data/db/migrate/20110208031356_add_delayed_jobs_tag.rb +14 -0
- data/db/migrate/20110426161613_add_delayed_jobs_max_attempts.rb +13 -0
- data/db/migrate/20110516225834_add_delayed_jobs_strand.rb +14 -0
- data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +26 -0
- data/db/migrate/20110610213249_optimize_delayed_jobs.rb +40 -0
- data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +52 -0
- data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +31 -0
- data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +15 -0
- data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +80 -0
- data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +15 -0
- data/db/migrate/20120608191051_add_jobs_run_at_index.rb +15 -0
- data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +13 -0
- data/db/migrate/20140505215131_add_failed_jobs_original_job_id.rb +13 -0
- data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +13 -0
- data/db/migrate/20140505223637_drop_failed_jobs_original_id.rb +13 -0
- data/db/migrate/20140512213941_add_source_to_jobs.rb +15 -0
- data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +70 -0
- data/db/migrate/20151123210429_add_expires_at_to_jobs.rb +15 -0
- data/db/migrate/20151210162949_improve_max_concurrent.rb +50 -0
- data/lib/delayed/backend/active_record.rb +340 -0
- data/lib/delayed/backend/base.rb +335 -0
- data/lib/delayed/backend/redis/bulk_update.lua +50 -0
- data/lib/delayed/backend/redis/destroy_job.lua +2 -0
- data/lib/delayed/backend/redis/enqueue.lua +29 -0
- data/lib/delayed/backend/redis/fail_job.lua +5 -0
- data/lib/delayed/backend/redis/find_available.lua +3 -0
- data/lib/delayed/backend/redis/functions.rb +57 -0
- data/lib/delayed/backend/redis/get_and_lock_next_available.lua +17 -0
- data/lib/delayed/backend/redis/includes/jobs_common.lua +203 -0
- data/lib/delayed/backend/redis/job.rb +497 -0
- data/lib/delayed/backend/redis/set_running.lua +5 -0
- data/lib/delayed/backend/redis/tickle_strand.lua +2 -0
- data/lib/delayed/batch.rb +56 -0
- data/lib/delayed/cli.rb +101 -0
- data/lib/delayed/daemon.rb +103 -0
- data/lib/delayed/engine.rb +4 -0
- data/lib/delayed/job_tracking.rb +31 -0
- data/lib/delayed/lifecycle.rb +90 -0
- data/lib/delayed/log_tailer.rb +22 -0
- data/lib/delayed/message_sending.rb +134 -0
- data/lib/delayed/performable_method.rb +52 -0
- data/lib/delayed/periodic.rb +85 -0
- data/lib/delayed/plugin.rb +22 -0
- data/lib/delayed/pool.rb +161 -0
- data/lib/delayed/server/helpers.rb +28 -0
- data/lib/delayed/server/public/css/app.css +12 -0
- data/lib/delayed/server/public/js/app.js +132 -0
- data/lib/delayed/server/views/index.erb +90 -0
- data/lib/delayed/server/views/layout.erb +47 -0
- data/lib/delayed/server.rb +120 -0
- data/lib/delayed/settings.rb +90 -0
- data/lib/delayed/testing.rb +32 -0
- data/lib/delayed/version.rb +3 -0
- data/lib/delayed/work_queue/in_process.rb +13 -0
- data/lib/delayed/work_queue/parent_process.rb +180 -0
- data/lib/delayed/worker.rb +234 -0
- data/lib/delayed/yaml_extensions.rb +109 -0
- data/lib/delayed_job.rb +46 -0
- data/lib/inst-jobs.rb +1 -0
- data/spec/active_record_job_spec.rb +246 -0
- data/spec/delayed/cli_spec.rb +23 -0
- data/spec/delayed/daemon_spec.rb +35 -0
- data/spec/delayed/server_spec.rb +63 -0
- data/spec/delayed/settings_spec.rb +32 -0
- data/spec/delayed/work_queue/in_process_spec.rb +31 -0
- data/spec/delayed/work_queue/parent_process_spec.rb +159 -0
- data/spec/delayed/worker_spec.rb +16 -0
- data/spec/gemfiles/32.gemfile +6 -0
- data/spec/gemfiles/40.gemfile +5 -0
- data/spec/gemfiles/41.gemfile +5 -0
- data/spec/gemfiles/42.gemfile +5 -0
- data/spec/migrate/20140924140513_add_story_table.rb +7 -0
- data/spec/redis_job_spec.rb +140 -0
- data/spec/sample_jobs.rb +28 -0
- data/spec/shared/delayed_batch.rb +85 -0
- data/spec/shared/delayed_method.rb +419 -0
- data/spec/shared/performable_method.rb +66 -0
- data/spec/shared/shared_backend.rb +819 -0
- data/spec/shared/testing.rb +48 -0
- data/spec/shared/worker.rb +378 -0
- data/spec/shared_jobs_specs.rb +15 -0
- data/spec/spec_helper.rb +97 -0
- metadata +390 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
module Delayed
|
|
4
|
+
# Daemon controls the parent proces that runs the Pool and monitors the Worker processes.
|
|
5
|
+
class Daemon
|
|
6
|
+
attr_reader :pid_folder
|
|
7
|
+
|
|
8
|
+
def initialize(pid_folder)
|
|
9
|
+
@pid_folder = pid_folder
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def status(print: true, pid: self.pid)
|
|
13
|
+
alive = pid && (Process.kill(0, pid) rescue false) && :running
|
|
14
|
+
alive ||= :draining if pid && Process.kill(0, -pid) rescue false
|
|
15
|
+
if alive
|
|
16
|
+
puts "Delayed jobs #{alive}, pool PID: #{pid}" if print
|
|
17
|
+
else
|
|
18
|
+
puts "No delayed jobs pool running" if print && print != :alive
|
|
19
|
+
end
|
|
20
|
+
alive
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def daemonize!
|
|
24
|
+
FileUtils.mkdir_p(pid_folder)
|
|
25
|
+
puts "Daemonizing..."
|
|
26
|
+
|
|
27
|
+
exit if fork
|
|
28
|
+
Process.setsid
|
|
29
|
+
exit if fork
|
|
30
|
+
Process.setpgrp
|
|
31
|
+
|
|
32
|
+
@daemon = true
|
|
33
|
+
lock_file = File.open(pid_file, 'wb')
|
|
34
|
+
# someone else is already running; just exit
|
|
35
|
+
unless lock_file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
at_exit { lock_file.flock(File::LOCK_UN) }
|
|
39
|
+
lock_file.puts(Process.pid.to_s)
|
|
40
|
+
lock_file.flush
|
|
41
|
+
|
|
42
|
+
# if we blow up so badly that we can't syslog the error, try to send
|
|
43
|
+
# it somewhere useful
|
|
44
|
+
last_ditch_logfile = Settings.last_ditch_logfile || "log/delayed_job.log"
|
|
45
|
+
if last_ditch_logfile[0] != '|'
|
|
46
|
+
last_ditch_logfile = Settings.expand_rails_path(last_ditch_logfile)
|
|
47
|
+
end
|
|
48
|
+
STDIN.reopen("/dev/null")
|
|
49
|
+
STDOUT.reopen(open(last_ditch_logfile, 'a'))
|
|
50
|
+
STDERR.reopen(STDOUT)
|
|
51
|
+
STDOUT.sync = STDERR.sync = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# stop the currently running daemon (not this current process, the one in the pid_file)
|
|
55
|
+
def stop(kill: false, pid: self.pid)
|
|
56
|
+
alive = status(pid: pid, print: false)
|
|
57
|
+
if alive == :running || (kill && alive == :draining)
|
|
58
|
+
puts "Stopping pool #{pid}..."
|
|
59
|
+
signal = 'INT'
|
|
60
|
+
if kill
|
|
61
|
+
pid = -pid # send to the whole group
|
|
62
|
+
if kill == 9
|
|
63
|
+
signal = 'KILL'
|
|
64
|
+
else
|
|
65
|
+
signal = 'TERM'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
begin
|
|
69
|
+
Process.kill(signal, pid)
|
|
70
|
+
rescue Errno::ESRCH
|
|
71
|
+
# ignore if the pid no longer exists
|
|
72
|
+
end
|
|
73
|
+
wait(kill)
|
|
74
|
+
else
|
|
75
|
+
status
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def wait(kill)
|
|
80
|
+
if kill
|
|
81
|
+
sleep(0.5) while status(pid: pid, print: false)
|
|
82
|
+
else
|
|
83
|
+
sleep(0.5) while status(pid: pid, print: false) == :running
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def pid_file
|
|
88
|
+
File.join(pid_folder, 'delayed_jobs_pool.pid')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def pid
|
|
92
|
+
if File.file?(pid_file)
|
|
93
|
+
pid = File.read(pid_file).to_i
|
|
94
|
+
pid = nil unless pid > 0
|
|
95
|
+
end
|
|
96
|
+
pid
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def daemonized?
|
|
100
|
+
!!@daemon
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
# Used when a block of code wants to track what jobs are created,
|
|
3
|
+
# for instance in tests.
|
|
4
|
+
# Delayed::Job.track_jobs { ...block... } returns a JobTracking object
|
|
5
|
+
# Right now this just tracks created jobs, it could be expanded to track a
|
|
6
|
+
# lot more about what's going on in Delayed Jobs as it's needed.
|
|
7
|
+
class JobTracking < Struct.new(:created)
|
|
8
|
+
def self.track
|
|
9
|
+
@current_tracking = self.new
|
|
10
|
+
yield
|
|
11
|
+
tracking = @current_tracking
|
|
12
|
+
@current_tracking = nil
|
|
13
|
+
tracking
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.job_created(job)
|
|
17
|
+
@current_tracking.try(:job_created, job)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def job_created(job)
|
|
21
|
+
return unless job
|
|
22
|
+
@lock.synchronize { self.created << job }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
super
|
|
27
|
+
self.created = []
|
|
28
|
+
@lock = Mutex.new
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
class InvalidCallback < Exception; end
|
|
3
|
+
|
|
4
|
+
class Lifecycle
|
|
5
|
+
EVENTS = {
|
|
6
|
+
:error => [:worker, :job, :exception],
|
|
7
|
+
:exceptional_exit => [:worker, :exception],
|
|
8
|
+
:execute => [:worker],
|
|
9
|
+
:invoke_job => [:job],
|
|
10
|
+
:loop => [:worker],
|
|
11
|
+
:perform => [:worker, :job],
|
|
12
|
+
:pop => [:worker],
|
|
13
|
+
:work_queue_pop => [:work_queue],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
reset!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@callbacks = EVENTS.keys.inject({}) { |hash, e| hash[e] = Callback.new; hash }
|
|
22
|
+
Delayed::Worker.plugins.each { |plugin| plugin.reset! }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def before(event, &block)
|
|
26
|
+
add(:before, event, &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def after(event, &block)
|
|
30
|
+
add(:after, event, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def around(event, &block)
|
|
34
|
+
add(:around, event, &block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run_callbacks(event, *args, &block)
|
|
38
|
+
missing_callback(event) unless @callbacks.has_key?(event)
|
|
39
|
+
|
|
40
|
+
unless EVENTS[event].size == args.size
|
|
41
|
+
raise ArgumentError, "Callback #{event} expects #{EVENTS[event].size} parameter(s): #{EVENTS[event].join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@callbacks[event].execute(*args, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def add(type, event, &block)
|
|
50
|
+
missing_callback(event) unless @callbacks.has_key?(event)
|
|
51
|
+
|
|
52
|
+
@callbacks[event].add(type, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def missing_callback(event)
|
|
56
|
+
raise InvalidCallback, "Unknown callback event: #{event}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Callback
|
|
61
|
+
def initialize
|
|
62
|
+
@before = []
|
|
63
|
+
@after = []
|
|
64
|
+
|
|
65
|
+
# Identity proc. Avoids special cases when there is no existing around chain.
|
|
66
|
+
@around = lambda { |*args, &block| block.call(*args) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def execute(*args, &block)
|
|
70
|
+
@before.each { |c| c.call(*args) }
|
|
71
|
+
result = @around.call(*args, &block)
|
|
72
|
+
@after.each { |c| c.call(*args) }
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add(type, &callback)
|
|
77
|
+
case type
|
|
78
|
+
when :before
|
|
79
|
+
@before << callback
|
|
80
|
+
when :after
|
|
81
|
+
@after << callback
|
|
82
|
+
when :around
|
|
83
|
+
chain = @around # use a local variable so that the current chain is closed over in the following lambda
|
|
84
|
+
@around = lambda { |*a, &block| chain.call(*a) { |*b| callback.call(*b, &block) } }
|
|
85
|
+
else
|
|
86
|
+
raise InvalidCallback, "Invalid callback type: #{type}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
class LogTailer
|
|
3
|
+
def run
|
|
4
|
+
if Rails.logger.respond_to?(:log_path)
|
|
5
|
+
log_path = Rails.logger.log_path
|
|
6
|
+
elsif Rails.logger.instance_variable_get('@logdev').try(:instance_variable_get, '@dev').try(:path)
|
|
7
|
+
log_path = Rails.logger.instance_variable_get('@logdev').instance_variable_get('@dev').path
|
|
8
|
+
else
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
Rails.logger.auto_flushing = true if Rails.logger.respond_to?(:auto_flushing=)
|
|
12
|
+
Thread.new do
|
|
13
|
+
f = File.open(log_path, 'r')
|
|
14
|
+
f.seek(0, IO::SEEK_END)
|
|
15
|
+
loop do
|
|
16
|
+
content = f.read
|
|
17
|
+
content.present? ? STDOUT.print(content) : sleep(0.5)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
module MessageSending
|
|
3
|
+
def send_later(method, *args)
|
|
4
|
+
send_later_enqueue_args(method, {}, *args)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def send_later_enqueue_args(method, enqueue_args = {}, *args)
|
|
8
|
+
enqueue_args = enqueue_args.dup
|
|
9
|
+
# support procs/methods as enqueue arguments
|
|
10
|
+
enqueue_args.each do |k,v|
|
|
11
|
+
if v.respond_to?(:call)
|
|
12
|
+
enqueue_args[k] = v.call(self)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
no_delay = enqueue_args.delete(:no_delay)
|
|
17
|
+
on_failure = enqueue_args.delete(:on_failure)
|
|
18
|
+
on_permanent_failure = enqueue_args.delete(:on_permanent_failure)
|
|
19
|
+
if !no_delay
|
|
20
|
+
# delay queuing up the job in another database until the results of the current
|
|
21
|
+
# transaction are visible
|
|
22
|
+
connection = self.class.connection if self.class.respond_to?(:connection)
|
|
23
|
+
connection ||= self.connection if respond_to?(:connection)
|
|
24
|
+
connection ||= ActiveRecord::Base.connection
|
|
25
|
+
|
|
26
|
+
if (Delayed::Job != Delayed::Backend::ActiveRecord::Job || connection != Delayed::Job.connection)
|
|
27
|
+
connection.after_transaction_commit do
|
|
28
|
+
Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args,
|
|
29
|
+
on_failure, on_permanent_failure), enqueue_args)
|
|
30
|
+
end
|
|
31
|
+
return nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
result = Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args,
|
|
36
|
+
on_failure, on_permanent_failure), enqueue_args)
|
|
37
|
+
result = nil unless no_delay
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def send_later_with_queue(method, queue, *args)
|
|
42
|
+
send_later_enqueue_args(method, { :queue => queue }, *args)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def send_at(time, method, *args)
|
|
46
|
+
send_later_enqueue_args(method,
|
|
47
|
+
{ :run_at => time }, *args)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def send_at_with_queue(time, method, queue, *args)
|
|
51
|
+
send_later_enqueue_args(method,
|
|
52
|
+
{ :run_at => time, :queue => queue },
|
|
53
|
+
*args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def send_later_unless_in_job(method, *args)
|
|
57
|
+
if Delayed::Job.in_delayed_job?
|
|
58
|
+
send(method, *args)
|
|
59
|
+
else
|
|
60
|
+
send_later(method, *args)
|
|
61
|
+
end
|
|
62
|
+
nil # can't rely on the type of return value, so return nothing
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_later_if_production(*args)
|
|
66
|
+
if Rails.env.production?
|
|
67
|
+
send_later(*args)
|
|
68
|
+
else
|
|
69
|
+
send(*args)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def send_later_if_production_enqueue_args(method, enqueue_args, *args)
|
|
74
|
+
if Rails.env.production?
|
|
75
|
+
send_later_enqueue_args(method, enqueue_args, *args)
|
|
76
|
+
else
|
|
77
|
+
send(method, *args)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def send_now_or_later(_when, *args)
|
|
82
|
+
if _when == :now
|
|
83
|
+
send(*args)
|
|
84
|
+
else
|
|
85
|
+
send_later(*args)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_now_or_later_if_production(_when, *args)
|
|
90
|
+
if _when == :now
|
|
91
|
+
send(*args)
|
|
92
|
+
else
|
|
93
|
+
send_later_if_production(*args)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
module ClassMethods
|
|
98
|
+
def add_send_later_methods(method, enqueue_args={}, default_async=false)
|
|
99
|
+
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
|
100
|
+
|
|
101
|
+
with_method, without_method = "#{aliased_method}_with_send_later#{punctuation}", "#{aliased_method}_without_send_later#{punctuation}"
|
|
102
|
+
|
|
103
|
+
define_method(with_method) do |*args|
|
|
104
|
+
send_later_enqueue_args(without_method, enqueue_args, *args)
|
|
105
|
+
end
|
|
106
|
+
alias_method without_method, method
|
|
107
|
+
|
|
108
|
+
if default_async
|
|
109
|
+
alias_method method, with_method
|
|
110
|
+
case
|
|
111
|
+
when public_method_defined?(without_method)
|
|
112
|
+
public method
|
|
113
|
+
when protected_method_defined?(without_method)
|
|
114
|
+
protected method
|
|
115
|
+
when private_method_defined?(without_method)
|
|
116
|
+
private method
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_asynchronously(method, enqueue_args={})
|
|
122
|
+
add_send_later_methods(method, enqueue_args, true)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def handle_asynchronously_with_queue(method, queue)
|
|
126
|
+
add_send_later_methods(method, {:queue => queue}, true)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def handle_asynchronously_if_production(method, enqueue_args={})
|
|
130
|
+
add_send_later_methods(method, enqueue_args, Rails.env.production?)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
class PerformableMethod < Struct.new(:object, :method, :args, :fail_cb, :permanent_fail_cb)
|
|
3
|
+
def initialize(object, method, args = [], fail_cb = nil, permanent_fail_cb = nil)
|
|
4
|
+
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method, true)
|
|
5
|
+
|
|
6
|
+
self.object = object
|
|
7
|
+
self.args = args
|
|
8
|
+
self.method = method.to_sym
|
|
9
|
+
self.fail_cb = fail_cb
|
|
10
|
+
self.permanent_fail_cb = permanent_fail_cb
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def display_name
|
|
14
|
+
if object.is_a?(Module)
|
|
15
|
+
"#{object}.#{method}"
|
|
16
|
+
else
|
|
17
|
+
"#{object.class}##{method}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
alias_method :tag, :display_name
|
|
21
|
+
|
|
22
|
+
def perform
|
|
23
|
+
object.send(method, *args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_failure(error)
|
|
27
|
+
object.send(fail_cb, error) if fail_cb
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_permanent_failure(error)
|
|
31
|
+
object.send(permanent_fail_cb, error) if permanent_fail_cb
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def deep_de_ar_ize(arg)
|
|
35
|
+
case arg
|
|
36
|
+
when Hash
|
|
37
|
+
"{#{arg.map { |k, v| "#{deep_de_ar_ize(k)} => #{deep_de_ar_ize(v)}" }.join(', ')}}"
|
|
38
|
+
when Array
|
|
39
|
+
"[#{arg.map { |a| deep_de_ar_ize(a) }.join(', ')}]"
|
|
40
|
+
when ActiveRecord::Base
|
|
41
|
+
"#{arg.class}.find(#{arg.id})"
|
|
42
|
+
else
|
|
43
|
+
arg.inspect
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def full_name
|
|
48
|
+
obj_name = object.is_a?(ActiveRecord::Base) ? "#{object.class}.find(#{object.id}).#{method}" : display_name
|
|
49
|
+
"#{obj_name}(#{args.map { |a| deep_de_ar_ize(a) }.join(', ')})"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'rufus/scheduler'
|
|
2
|
+
|
|
3
|
+
module Delayed
|
|
4
|
+
class Periodic
|
|
5
|
+
attr_reader :name, :cron
|
|
6
|
+
|
|
7
|
+
yaml_as "tag:ruby.yaml.org,2002:Delayed::Periodic"
|
|
8
|
+
|
|
9
|
+
def to_yaml(opts = {})
|
|
10
|
+
YAML.quick_emit(self.object_id, opts) { |out| out.scalar(taguri, @name) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def encode_with(coder)
|
|
14
|
+
coder.scalar("!ruby/Delayed::Periodic", @name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.yaml_new(klass, tag, val)
|
|
18
|
+
self.scheduled[val] || raise(NameError, "job #{val} is no longer scheduled")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
cattr_accessor :scheduled, :overrides
|
|
22
|
+
self.scheduled = {}
|
|
23
|
+
self.overrides = {}
|
|
24
|
+
|
|
25
|
+
def self.add_overrides(overrides)
|
|
26
|
+
overrides.each do |name, cron_line|
|
|
27
|
+
# throws error if the line is malformed
|
|
28
|
+
Rufus::Scheduler::CronLine.new(cron_line)
|
|
29
|
+
end
|
|
30
|
+
self.overrides.merge!(overrides)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
STRAND = 'periodic scheduling'
|
|
34
|
+
|
|
35
|
+
def self.cron(job_name, cron_line, job_args = {}, &block)
|
|
36
|
+
raise ArgumentError, "job #{job_name} already scheduled!" if self.scheduled[job_name]
|
|
37
|
+
cron_line = overrides[job_name] || cron_line
|
|
38
|
+
self.scheduled[job_name] = self.new(job_name, cron_line, job_args, block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.audit_queue
|
|
42
|
+
# we used to queue up a job in a strand here, and perform the audit inside that job
|
|
43
|
+
# however, now that we're using singletons for scheduling periodic jobs,
|
|
44
|
+
# it's fine to just do the audit in-line here without risk of creating duplicates
|
|
45
|
+
perform_audit!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# make sure all periodic jobs are scheduled for their next run in the job queue
|
|
49
|
+
# this auditing should run on the strand
|
|
50
|
+
def self.perform_audit!
|
|
51
|
+
self.scheduled.each { |name, periodic| periodic.enqueue }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(name, cron_line, job_args, block)
|
|
55
|
+
@name = name
|
|
56
|
+
@cron = Rufus::Scheduler::CronLine.new(cron_line)
|
|
57
|
+
@job_args = { :priority => Delayed::LOW_PRIORITY }.merge(job_args.symbolize_keys)
|
|
58
|
+
@block = block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def enqueue
|
|
62
|
+
Delayed::Job.enqueue(self, @job_args.merge(:max_attempts => 1, :run_at => @cron.next_time(Delayed::Periodic.now), :singleton => tag))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def perform
|
|
66
|
+
@block.call()
|
|
67
|
+
ensure
|
|
68
|
+
begin
|
|
69
|
+
enqueue
|
|
70
|
+
rescue
|
|
71
|
+
# double fail! the auditor will have to catch this.
|
|
72
|
+
Rails.logger.error "Failure enqueueing periodic job! #{@name} #{$!.inspect}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def tag
|
|
77
|
+
"periodic: #{@name}"
|
|
78
|
+
end
|
|
79
|
+
alias_method :display_name, :tag
|
|
80
|
+
|
|
81
|
+
def self.now
|
|
82
|
+
Time.zone.now
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
|
2
|
+
|
|
3
|
+
module Delayed
|
|
4
|
+
class Plugin
|
|
5
|
+
class_attribute :callback_block
|
|
6
|
+
|
|
7
|
+
def self.callbacks(&block)
|
|
8
|
+
self.callback_block = block
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.inject!
|
|
12
|
+
unless @injected
|
|
13
|
+
self.callback_block.call(Delayed::Worker.lifecycle) if self.callback_block
|
|
14
|
+
end
|
|
15
|
+
@injected = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.reset!
|
|
19
|
+
@injected = false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/delayed/pool.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Delayed
|
|
2
|
+
class Pool
|
|
3
|
+
mattr_accessor :on_fork
|
|
4
|
+
self.on_fork = ->{ }
|
|
5
|
+
|
|
6
|
+
attr_reader :workers
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
if args.first.is_a?(Hash)
|
|
10
|
+
@config = args.first
|
|
11
|
+
else
|
|
12
|
+
warn "Calling Delayed::Pool.new directly is deprecated. Use `Delayed::CLI.new.run()` instead."
|
|
13
|
+
end
|
|
14
|
+
@workers = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
warn "Delayed::Pool#run is deprecated and will be removed. Use `Delayed::CLI.new.run()` instead."
|
|
19
|
+
Delayed::CLI.new.run()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
say "Started job master", :info
|
|
24
|
+
$0 = procname
|
|
25
|
+
# fork to handle unlocking (to prevent polluting the parent with worker objects)
|
|
26
|
+
unlock_pid = fork_with_reconnects do
|
|
27
|
+
unlock_orphaned_jobs
|
|
28
|
+
end
|
|
29
|
+
Process.wait unlock_pid
|
|
30
|
+
|
|
31
|
+
spawn_periodic_auditor
|
|
32
|
+
spawn_all_workers
|
|
33
|
+
say "Workers spawned"
|
|
34
|
+
join
|
|
35
|
+
say "Shutting down"
|
|
36
|
+
rescue Interrupt => e
|
|
37
|
+
say "Signal received, exiting", :info
|
|
38
|
+
rescue Exception => e
|
|
39
|
+
say "Job master died with error: #{e.inspect}\n#{e.backtrace.join("\n")}", :fatal
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def procname
|
|
46
|
+
"delayed_jobs_pool#{Settings.pool_procname_suffix}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def say(msg, level = :debug)
|
|
50
|
+
if defined?(Rails.logger) && Rails.logger
|
|
51
|
+
Rails.logger.send(level, "[#{Process.pid}]P #{msg}")
|
|
52
|
+
else
|
|
53
|
+
puts(msg)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unlock_orphaned_jobs(worker = nil, pid = nil)
|
|
58
|
+
return if Settings.disable_automatic_orphan_unlocking
|
|
59
|
+
|
|
60
|
+
unlocked_jobs = Delayed::Job.unlock_orphaned_jobs(pid)
|
|
61
|
+
say "Unlocked #{unlocked_jobs} orphaned jobs" if unlocked_jobs > 0
|
|
62
|
+
ActiveRecord::Base.connection_handler.clear_all_connections! unless Rails.env.test?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def spawn_all_workers
|
|
66
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!
|
|
67
|
+
|
|
68
|
+
if @config[:work_queue] == 'parent_process'
|
|
69
|
+
@work_queue = WorkQueue::ParentProcess.new
|
|
70
|
+
spawn_work_queue
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@config[:workers].each do |worker_config|
|
|
74
|
+
(worker_config[:workers] || 1).times { spawn_worker(worker_config) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def spawn_work_queue
|
|
79
|
+
parent_pid = Process.pid
|
|
80
|
+
pid = fork_with_reconnects do
|
|
81
|
+
$0 = "delayed_jobs_work_queue#{Settings.pool_procname_suffix}"
|
|
82
|
+
@work_queue.server(parent_pid: parent_pid).run
|
|
83
|
+
end
|
|
84
|
+
workers[pid] = :work_queue
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def spawn_worker(worker_config)
|
|
88
|
+
if worker_config[:periodic]
|
|
89
|
+
return # backwards compat
|
|
90
|
+
else
|
|
91
|
+
worker_config[:parent_pid] = Process.pid
|
|
92
|
+
worker_config[:work_queue] = @work_queue.client if @work_queue
|
|
93
|
+
worker = Delayed::Worker.new(worker_config)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
pid = fork_with_reconnects do
|
|
97
|
+
worker.start
|
|
98
|
+
end
|
|
99
|
+
workers[pid] = worker
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# child processes need to reconnect so they don't accidentally share redis or
|
|
103
|
+
# db connections with the parent
|
|
104
|
+
def fork_with_reconnects
|
|
105
|
+
fork do
|
|
106
|
+
Pool.on_fork.()
|
|
107
|
+
Delayed::Job.reconnect!
|
|
108
|
+
yield
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def spawn_periodic_auditor
|
|
113
|
+
return if Settings.disable_periodic_jobs
|
|
114
|
+
|
|
115
|
+
@periodic_thread = Thread.new do
|
|
116
|
+
# schedule the initial audit immediately on startup
|
|
117
|
+
schedule_periodic_audit
|
|
118
|
+
# initial sleep is randomized, for some staggering in the audit calls
|
|
119
|
+
# since job processors are usually all restarted at the same time
|
|
120
|
+
sleep(rand(15 * 60))
|
|
121
|
+
loop do
|
|
122
|
+
schedule_periodic_audit
|
|
123
|
+
sleep(15 * 60)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def schedule_periodic_audit
|
|
129
|
+
pid = fork_with_reconnects do
|
|
130
|
+
# we want to avoid db connections in the main pool process
|
|
131
|
+
$0 = "delayed_periodic_audit_scheduler"
|
|
132
|
+
Delayed::Periodic.audit_queue
|
|
133
|
+
end
|
|
134
|
+
workers[pid] = :periodic_audit
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def join
|
|
138
|
+
loop do
|
|
139
|
+
child = Process.wait
|
|
140
|
+
if workers.include?(child)
|
|
141
|
+
worker = workers.delete(child)
|
|
142
|
+
case worker
|
|
143
|
+
when :periodic_audit
|
|
144
|
+
say "ran auditor: #{worker}"
|
|
145
|
+
when :work_queue
|
|
146
|
+
say "work queue exited, restarting", :info
|
|
147
|
+
spawn_work_queue
|
|
148
|
+
else
|
|
149
|
+
say "child exited: #{child}, restarting", :info
|
|
150
|
+
# fork to handle unlocking (to prevent polluting the parent with worker objects)
|
|
151
|
+
unlock_pid = fork_with_reconnects do
|
|
152
|
+
unlock_orphaned_jobs(worker, child)
|
|
153
|
+
end
|
|
154
|
+
Process.wait unlock_pid
|
|
155
|
+
spawn_worker(worker.config)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|