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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/bin/inst_job +4 -0
  3. data/db/migrate/20101216224513_create_delayed_jobs.rb +40 -0
  4. data/db/migrate/20110208031356_add_delayed_jobs_tag.rb +14 -0
  5. data/db/migrate/20110426161613_add_delayed_jobs_max_attempts.rb +13 -0
  6. data/db/migrate/20110516225834_add_delayed_jobs_strand.rb +14 -0
  7. data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +26 -0
  8. data/db/migrate/20110610213249_optimize_delayed_jobs.rb +40 -0
  9. data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +52 -0
  10. data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +31 -0
  11. data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +15 -0
  12. data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +80 -0
  13. data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +15 -0
  14. data/db/migrate/20120608191051_add_jobs_run_at_index.rb +15 -0
  15. data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +13 -0
  16. data/db/migrate/20140505215131_add_failed_jobs_original_job_id.rb +13 -0
  17. data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +13 -0
  18. data/db/migrate/20140505223637_drop_failed_jobs_original_id.rb +13 -0
  19. data/db/migrate/20140512213941_add_source_to_jobs.rb +15 -0
  20. data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +70 -0
  21. data/db/migrate/20151123210429_add_expires_at_to_jobs.rb +15 -0
  22. data/db/migrate/20151210162949_improve_max_concurrent.rb +50 -0
  23. data/lib/delayed/backend/active_record.rb +340 -0
  24. data/lib/delayed/backend/base.rb +335 -0
  25. data/lib/delayed/backend/redis/bulk_update.lua +50 -0
  26. data/lib/delayed/backend/redis/destroy_job.lua +2 -0
  27. data/lib/delayed/backend/redis/enqueue.lua +29 -0
  28. data/lib/delayed/backend/redis/fail_job.lua +5 -0
  29. data/lib/delayed/backend/redis/find_available.lua +3 -0
  30. data/lib/delayed/backend/redis/functions.rb +57 -0
  31. data/lib/delayed/backend/redis/get_and_lock_next_available.lua +17 -0
  32. data/lib/delayed/backend/redis/includes/jobs_common.lua +203 -0
  33. data/lib/delayed/backend/redis/job.rb +497 -0
  34. data/lib/delayed/backend/redis/set_running.lua +5 -0
  35. data/lib/delayed/backend/redis/tickle_strand.lua +2 -0
  36. data/lib/delayed/batch.rb +56 -0
  37. data/lib/delayed/cli.rb +101 -0
  38. data/lib/delayed/daemon.rb +103 -0
  39. data/lib/delayed/engine.rb +4 -0
  40. data/lib/delayed/job_tracking.rb +31 -0
  41. data/lib/delayed/lifecycle.rb +90 -0
  42. data/lib/delayed/log_tailer.rb +22 -0
  43. data/lib/delayed/message_sending.rb +134 -0
  44. data/lib/delayed/performable_method.rb +52 -0
  45. data/lib/delayed/periodic.rb +85 -0
  46. data/lib/delayed/plugin.rb +22 -0
  47. data/lib/delayed/pool.rb +161 -0
  48. data/lib/delayed/server/helpers.rb +28 -0
  49. data/lib/delayed/server/public/css/app.css +12 -0
  50. data/lib/delayed/server/public/js/app.js +132 -0
  51. data/lib/delayed/server/views/index.erb +90 -0
  52. data/lib/delayed/server/views/layout.erb +47 -0
  53. data/lib/delayed/server.rb +120 -0
  54. data/lib/delayed/settings.rb +90 -0
  55. data/lib/delayed/testing.rb +32 -0
  56. data/lib/delayed/version.rb +3 -0
  57. data/lib/delayed/work_queue/in_process.rb +13 -0
  58. data/lib/delayed/work_queue/parent_process.rb +180 -0
  59. data/lib/delayed/worker.rb +234 -0
  60. data/lib/delayed/yaml_extensions.rb +109 -0
  61. data/lib/delayed_job.rb +46 -0
  62. data/lib/inst-jobs.rb +1 -0
  63. data/spec/active_record_job_spec.rb +246 -0
  64. data/spec/delayed/cli_spec.rb +23 -0
  65. data/spec/delayed/daemon_spec.rb +35 -0
  66. data/spec/delayed/server_spec.rb +63 -0
  67. data/spec/delayed/settings_spec.rb +32 -0
  68. data/spec/delayed/work_queue/in_process_spec.rb +31 -0
  69. data/spec/delayed/work_queue/parent_process_spec.rb +159 -0
  70. data/spec/delayed/worker_spec.rb +16 -0
  71. data/spec/gemfiles/32.gemfile +6 -0
  72. data/spec/gemfiles/40.gemfile +5 -0
  73. data/spec/gemfiles/41.gemfile +5 -0
  74. data/spec/gemfiles/42.gemfile +5 -0
  75. data/spec/migrate/20140924140513_add_story_table.rb +7 -0
  76. data/spec/redis_job_spec.rb +140 -0
  77. data/spec/sample_jobs.rb +28 -0
  78. data/spec/shared/delayed_batch.rb +85 -0
  79. data/spec/shared/delayed_method.rb +419 -0
  80. data/spec/shared/performable_method.rb +66 -0
  81. data/spec/shared/shared_backend.rb +819 -0
  82. data/spec/shared/testing.rb +48 -0
  83. data/spec/shared/worker.rb +378 -0
  84. data/spec/shared_jobs_specs.rb +15 -0
  85. data/spec/spec_helper.rb +97 -0
  86. 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,4 @@
1
+ module Delayed
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ 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
@@ -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