delayed_job 4.0.2 → 4.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/CHANGELOG.md +7 -0
- data/README.md +12 -7
- data/Rakefile +6 -1
- data/delayed_job.gemspec +5 -5
- data/lib/delayed/backend/base.rb +18 -20
- data/lib/delayed/backend/shared_spec.rb +139 -138
- data/lib/delayed/command.rb +75 -40
- data/lib/delayed/exceptions.rb +2 -1
- data/lib/delayed/lifecycle.rb +12 -11
- data/lib/delayed/message_sending.rb +9 -10
- data/lib/delayed/performable_mailer.rb +2 -2
- data/lib/delayed/performable_method.rb +2 -2
- data/lib/delayed/psych_ext.rb +29 -93
- data/lib/delayed/recipes.rb +5 -5
- data/lib/delayed/serialization/active_record.rb +11 -9
- data/lib/delayed/syck_ext.rb +3 -3
- data/lib/delayed/tasks.rb +5 -5
- data/lib/delayed/worker.rb +42 -42
- data/lib/generators/delayed_job/delayed_job_generator.rb +2 -3
- data/spec/delayed/backend/test.rb +22 -17
- data/spec/delayed/command_spec.rb +57 -0
- data/spec/helper.rb +25 -12
- data/spec/lifecycle_spec.rb +23 -15
- data/spec/message_sending_spec.rb +34 -34
- data/spec/performable_mailer_spec.rb +11 -11
- data/spec/performable_method_spec.rb +24 -26
- data/spec/psych_ext_spec.rb +12 -0
- data/spec/sample_jobs.rb +46 -18
- data/spec/test_backend_spec.rb +3 -3
- data/spec/worker_spec.rb +27 -27
- data/spec/yaml_ext_spec.rb +16 -16
- metadata +12 -8
data/lib/delayed/command.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
unless ENV['RAILS_ENV'] == 'test'
|
2
|
+
begin
|
3
|
+
require 'daemons'
|
4
|
+
rescue LoadError
|
5
|
+
raise "You need to add gem 'daemons' to your Gemfile if you wish to use it."
|
6
|
+
end
|
5
7
|
end
|
6
8
|
require 'optparse'
|
7
9
|
|
8
10
|
module Delayed
|
9
|
-
class Command
|
10
|
-
attr_accessor :worker_count
|
11
|
+
class Command # rubocop:disable ClassLength
|
12
|
+
attr_accessor :worker_count, :worker_pools
|
11
13
|
|
12
|
-
def initialize(args)
|
14
|
+
def initialize(args) # rubocop:disable MethodLength
|
13
15
|
@options = {
|
14
16
|
:quiet => true,
|
15
17
|
:pid_dir => "#{Rails.root}/tmp/pids"
|
@@ -18,88 +20,106 @@ module Delayed
|
|
18
20
|
@worker_count = 1
|
19
21
|
@monitor = false
|
20
22
|
|
21
|
-
opts = OptionParser.new do |
|
22
|
-
|
23
|
+
opts = OptionParser.new do |opt|
|
24
|
+
opt.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] start|stop|restart|run"
|
23
25
|
|
24
|
-
|
25
|
-
puts
|
26
|
+
opt.on('-h', '--help', 'Show this message') do
|
27
|
+
puts opt
|
26
28
|
exit 1
|
27
29
|
end
|
28
|
-
|
29
|
-
STDERR.puts
|
30
|
+
opt.on('-e', '--environment=NAME', 'Specifies the environment to run this delayed jobs under (test/development/production).') do |_e|
|
31
|
+
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'
|
30
32
|
end
|
31
|
-
|
33
|
+
opt.on('--min-priority N', 'Minimum priority of jobs to run.') do |n|
|
32
34
|
@options[:min_priority] = n
|
33
35
|
end
|
34
|
-
|
36
|
+
opt.on('--max-priority N', 'Maximum priority of jobs to run.') do |n|
|
35
37
|
@options[:max_priority] = n
|
36
38
|
end
|
37
|
-
|
38
|
-
@worker_count = worker_count.to_i rescue 1
|
39
|
+
opt.on('-n', '--number_of_workers=workers', 'Number of unique workers to spawn') do |worker_count|
|
40
|
+
@worker_count = worker_count.to_i rescue 1 # rubocop:disable RescueModifier
|
39
41
|
end
|
40
|
-
|
42
|
+
opt.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
|
41
43
|
@options[:pid_dir] = dir
|
42
44
|
end
|
43
|
-
|
45
|
+
opt.on('-i', '--identifier=n', 'A numeric identifier for the worker.') do |n|
|
44
46
|
@options[:identifier] = n
|
45
47
|
end
|
46
|
-
|
48
|
+
opt.on('-m', '--monitor', 'Start monitor process.') do
|
47
49
|
@monitor = true
|
48
50
|
end
|
49
|
-
|
51
|
+
opt.on('--sleep-delay N', 'Amount of time to sleep when no jobs are found') do |n|
|
50
52
|
@options[:sleep_delay] = n.to_i
|
51
53
|
end
|
52
|
-
|
54
|
+
opt.on('--read-ahead N', 'Number of jobs from the queue to consider') do |n|
|
53
55
|
@options[:read_ahead] = n
|
54
56
|
end
|
55
|
-
|
57
|
+
opt.on('-p', '--prefix NAME', 'String to be prefixed to worker process names') do |prefix|
|
56
58
|
@options[:prefix] = prefix
|
57
59
|
end
|
58
|
-
|
60
|
+
opt.on('--queues=queues', 'Specify which queue DJ must look up for jobs') do |queues|
|
59
61
|
@options[:queues] = queues.split(',')
|
60
62
|
end
|
61
|
-
|
63
|
+
opt.on('--queue=queue', 'Specify which queue DJ must look up for jobs') do |queue|
|
62
64
|
@options[:queues] = queue.split(',')
|
63
65
|
end
|
64
|
-
|
66
|
+
opt.on('--pool=queue1[,queue2][:worker_count]', 'Specify queues and number of workers for a worker pool') do |pool|
|
67
|
+
parse_worker_pool(pool)
|
68
|
+
end
|
69
|
+
opt.on('--exit-on-complete', 'Exit when no more jobs are available to run. This will exit if all jobs are scheduled to run in the future.') do
|
65
70
|
@options[:exit_on_complete] = true
|
66
71
|
end
|
67
72
|
end
|
68
73
|
@args = opts.parse!(args)
|
69
74
|
end
|
70
75
|
|
71
|
-
def daemonize
|
76
|
+
def daemonize # rubocop:disable PerceivedComplexity
|
72
77
|
dir = @options[:pid_dir]
|
73
|
-
Dir.mkdir(dir) unless File.
|
78
|
+
Dir.mkdir(dir) unless File.exist?(dir)
|
74
79
|
|
75
|
-
if
|
76
|
-
|
77
|
-
elsif @
|
78
|
-
|
79
|
-
|
80
|
+
if worker_pools
|
81
|
+
setup_pools
|
82
|
+
elsif @options[:identifier]
|
83
|
+
if worker_count > 1
|
84
|
+
raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
|
85
|
+
else
|
86
|
+
run_process("delayed_job.#{@options[:identifier]}", @options)
|
87
|
+
end
|
80
88
|
else
|
81
89
|
worker_count.times do |worker_index|
|
82
|
-
process_name = worker_count == 1 ?
|
83
|
-
run_process(process_name,
|
90
|
+
process_name = worker_count == 1 ? 'delayed_job' : "delayed_job.#{worker_index}"
|
91
|
+
run_process(process_name, @options)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_pools
|
97
|
+
worker_index = 0
|
98
|
+
@worker_pools.each do |queues, worker_count|
|
99
|
+
options = @options.merge(:queues => queues)
|
100
|
+
worker_count.times do
|
101
|
+
process_name = "delayed_job.#{worker_index}"
|
102
|
+
run_process(process_name, options)
|
103
|
+
worker_index += 1
|
84
104
|
end
|
85
105
|
end
|
86
106
|
end
|
87
107
|
|
88
|
-
def run_process(process_name,
|
108
|
+
def run_process(process_name, options = {})
|
89
109
|
Delayed::Worker.before_fork
|
90
|
-
Daemons.run_proc(process_name, :dir =>
|
91
|
-
$0 = File.join(
|
110
|
+
Daemons.run_proc(process_name, :dir => options[:pid_dir], :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*_args|
|
111
|
+
$0 = File.join(options[:prefix], process_name) if @options[:prefix]
|
92
112
|
run process_name
|
93
113
|
end
|
94
114
|
end
|
95
115
|
|
96
|
-
def run(worker_name = nil)
|
116
|
+
def run(worker_name = nil, options = {})
|
97
117
|
Dir.chdir(Rails.root)
|
98
118
|
|
99
119
|
Delayed::Worker.after_fork
|
100
120
|
Delayed::Worker.logger ||= Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
|
101
121
|
|
102
|
-
worker = Delayed::Worker.new(
|
122
|
+
worker = Delayed::Worker.new(options)
|
103
123
|
worker.name_prefix = "#{worker_name} "
|
104
124
|
worker.start
|
105
125
|
rescue => e
|
@@ -107,5 +127,20 @@ module Delayed
|
|
107
127
|
STDERR.puts e.message
|
108
128
|
exit 1
|
109
129
|
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def parse_worker_pool(pool)
|
134
|
+
@worker_pools ||= []
|
135
|
+
|
136
|
+
queues, worker_count = pool.split(':')
|
137
|
+
if ['*', '', nil].include?(queues)
|
138
|
+
queues = []
|
139
|
+
else
|
140
|
+
queues = queues.split(',')
|
141
|
+
end
|
142
|
+
worker_count = (worker_count || 1).to_i rescue 1
|
143
|
+
@worker_pools << [queues, worker_count]
|
144
|
+
end
|
110
145
|
end
|
111
146
|
end
|
data/lib/delayed/exceptions.rb
CHANGED
@@ -3,7 +3,8 @@ require 'timeout'
|
|
3
3
|
module Delayed
|
4
4
|
class WorkerTimeout < Timeout::Error
|
5
5
|
def message
|
6
|
-
|
6
|
+
seconds = Delayed::Worker.max_run_time.to_i
|
7
|
+
"#{super} (Delayed::Worker.max_run_time is only #{seconds} second#{seconds == 1 ? '' : 's'})"
|
7
8
|
end
|
8
9
|
end
|
9
10
|
|
data/lib/delayed/lifecycle.rb
CHANGED
@@ -13,7 +13,9 @@ module Delayed
|
|
13
13
|
}
|
14
14
|
|
15
15
|
def initialize
|
16
|
-
@callbacks = EVENTS.keys.
|
16
|
+
@callbacks = EVENTS.keys.each_with_object({}) do |e, hash|
|
17
|
+
hash[e] = Callback.new
|
18
|
+
end
|
17
19
|
end
|
18
20
|
|
19
21
|
def before(event, &block)
|
@@ -29,7 +31,7 @@ module Delayed
|
|
29
31
|
end
|
30
32
|
|
31
33
|
def run_callbacks(event, *args, &block)
|
32
|
-
missing_callback(event) unless @callbacks.
|
34
|
+
missing_callback(event) unless @callbacks.key?(event)
|
33
35
|
|
34
36
|
unless EVENTS[event].size == args.size
|
35
37
|
raise ArgumentError, "Callback #{event} expects #{EVENTS[event].size} parameter(s): #{EVENTS[event].join(', ')}"
|
@@ -38,17 +40,16 @@ module Delayed
|
|
38
40
|
@callbacks[event].execute(*args, &block)
|
39
41
|
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
def add(type, event, &block)
|
44
|
-
missing_callback(event) unless @callbacks.has_key?(event)
|
43
|
+
private
|
45
44
|
|
46
|
-
|
47
|
-
|
45
|
+
def add(type, event, &block)
|
46
|
+
missing_callback(event) unless @callbacks.key?(event)
|
47
|
+
@callbacks[event].add(type, &block)
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
def missing_callback(event)
|
51
|
+
raise InvalidCallback, "Unknown callback event: #{event}"
|
52
|
+
end
|
52
53
|
end
|
53
54
|
|
54
55
|
class Callback
|
@@ -17,31 +17,30 @@ module Delayed
|
|
17
17
|
def delay(options = {})
|
18
18
|
DelayProxy.new(PerformableMethod, self, options)
|
19
19
|
end
|
20
|
-
|
20
|
+
alias_method :__delay__, :delay
|
21
21
|
|
22
22
|
def send_later(method, *args)
|
23
|
-
warn
|
23
|
+
warn '[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method'
|
24
24
|
__delay__.__send__(method, *args)
|
25
25
|
end
|
26
26
|
|
27
27
|
def send_at(time, method, *args)
|
28
|
-
warn
|
28
|
+
warn '[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method'
|
29
29
|
__delay__(:run_at => time).__send__(method, *args)
|
30
30
|
end
|
31
31
|
|
32
32
|
module ClassMethods
|
33
33
|
def handle_asynchronously(method, opts = {})
|
34
|
-
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
34
|
+
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1 # rubocop:disable PerlBackrefs
|
35
35
|
with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
|
36
36
|
define_method(with_method) do |*args|
|
37
37
|
curr_opts = opts.clone
|
38
38
|
curr_opts.each_key do |key|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
39
|
+
next unless (val = curr_opts[key]).is_a?(Proc)
|
40
|
+
curr_opts[key] = if val.arity == 1
|
41
|
+
val.call(self)
|
42
|
+
else
|
43
|
+
val.call
|
45
44
|
end
|
46
45
|
end
|
47
46
|
delay(curr_opts).__send__(without_method, *args)
|
@@ -15,7 +15,7 @@ module Delayed
|
|
15
15
|
end
|
16
16
|
|
17
17
|
Mail::Message.class_eval do
|
18
|
-
def delay(*
|
19
|
-
raise
|
18
|
+
def delay(*_args)
|
19
|
+
raise 'Use MyMailer.delay.mailer_action(args) to delay sending of emails.'
|
20
20
|
end
|
21
21
|
end
|
@@ -10,7 +10,7 @@ module Delayed
|
|
10
10
|
raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
|
11
11
|
|
12
12
|
if object.respond_to?(:persisted?) && !object.persisted?
|
13
|
-
raise
|
13
|
+
raise ArgumentError, 'Jobs cannot be created for non-persisted records'
|
14
14
|
end
|
15
15
|
|
16
16
|
self.object = object
|
@@ -30,7 +30,7 @@ module Delayed
|
|
30
30
|
object.send(symbol, *args)
|
31
31
|
end
|
32
32
|
|
33
|
-
def respond_to?(symbol, include_private=false)
|
33
|
+
def respond_to?(symbol, include_private = false)
|
34
34
|
super || object.respond_to?(symbol, include_private)
|
35
35
|
end
|
36
36
|
end
|
data/lib/delayed/psych_ext.rb
CHANGED
@@ -1,133 +1,69 @@
|
|
1
1
|
if defined?(ActiveRecord)
|
2
2
|
ActiveRecord::Base.class_eval do
|
3
|
+
# rubocop:disable BlockNesting
|
3
4
|
if instance_methods.include?(:encode_with)
|
4
5
|
def encode_with_override(coder)
|
5
6
|
encode_with_without_override(coder)
|
6
|
-
coder.tag = "!ruby/ActiveRecord:#{self.class.name}"
|
7
|
+
coder.tag = "!ruby/ActiveRecord:#{self.class.name}" if coder.respond_to?(:tag=)
|
7
8
|
end
|
8
9
|
alias_method :encode_with_without_override, :encode_with
|
9
10
|
alias_method :encode_with, :encode_with_override
|
10
11
|
else
|
11
12
|
def encode_with(coder)
|
12
|
-
coder[
|
13
|
-
coder.tag = "!ruby/ActiveRecord:#{self.class.name}"
|
13
|
+
coder['attributes'] = attributes
|
14
|
+
coder.tag = "!ruby/ActiveRecord:#{self.class.name}" if coder.respond_to?(:tag=)
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
coder
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
module Delayed
|
21
|
+
class PerformableMethod
|
22
|
+
# serialize to YAML
|
23
|
+
def encode_with(coder)
|
24
|
+
coder.map = {
|
25
|
+
'object' => object,
|
26
|
+
'method_name' => method_name,
|
27
|
+
'args' => args
|
28
|
+
}
|
29
|
+
end
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
30
33
|
module Psych
|
31
34
|
module Visitors
|
32
|
-
class YAMLTree
|
33
|
-
def visit_Class(klass)
|
34
|
-
@emitter.scalar klass.name, nil, '!ruby/class', false, false, Nodes::Scalar::SINGLE_QUOTED
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
35
|
class ToRuby
|
39
|
-
def
|
40
|
-
@st[o.anchor] = o.value if o.anchor
|
41
|
-
|
42
|
-
if klass = Psych.load_tags[o.tag]
|
43
|
-
instance = klass.allocate
|
44
|
-
|
45
|
-
if instance.respond_to?(:init_with)
|
46
|
-
coder = Psych::Coder.new(o.tag)
|
47
|
-
coder.scalar = o.value
|
48
|
-
instance.init_with coder
|
49
|
-
end
|
50
|
-
|
51
|
-
return instance
|
52
|
-
end
|
53
|
-
|
54
|
-
return o.value if o.quoted
|
55
|
-
return @ss.tokenize(o.value) unless o.tag
|
56
|
-
|
57
|
-
case o.tag
|
58
|
-
when '!binary', 'tag:yaml.org,2002:binary'
|
59
|
-
o.value.unpack('m').first
|
60
|
-
when '!str', 'tag:yaml.org,2002:str'
|
61
|
-
o.value
|
62
|
-
when "!ruby/object:DateTime"
|
63
|
-
require 'date'
|
64
|
-
@ss.parse_time(o.value).to_datetime
|
65
|
-
when "!ruby/object:Complex"
|
66
|
-
Complex(o.value)
|
67
|
-
when "!ruby/object:Rational"
|
68
|
-
Rational(o.value)
|
69
|
-
when "!ruby/class", "!ruby/module"
|
70
|
-
resolve_class o.value
|
71
|
-
when "tag:yaml.org,2002:float", "!float"
|
72
|
-
Float(@ss.tokenize(o.value))
|
73
|
-
when "!ruby/regexp"
|
74
|
-
o.value =~ /^\/(.*)\/([mixn]*)$/
|
75
|
-
source = $1
|
76
|
-
options = 0
|
77
|
-
lang = nil
|
78
|
-
($2 || '').split('').each do |option|
|
79
|
-
case option
|
80
|
-
when 'x' then options |= Regexp::EXTENDED
|
81
|
-
when 'i' then options |= Regexp::IGNORECASE
|
82
|
-
when 'm' then options |= Regexp::MULTILINE
|
83
|
-
when 'n' then options |= Regexp::NOENCODING
|
84
|
-
else lang = option
|
85
|
-
end
|
86
|
-
end
|
87
|
-
Regexp.new(*[source, options, lang].compact)
|
88
|
-
when "!ruby/range"
|
89
|
-
args = o.value.split(/([.]{2,3})/, 2).map { |s|
|
90
|
-
accept Nodes::Scalar.new(s)
|
91
|
-
}
|
92
|
-
args.push(args.delete_at(1) == '...')
|
93
|
-
Range.new(*args)
|
94
|
-
when /^!ruby\/sym(bol)?:?(.*)?$/
|
95
|
-
o.value.to_sym
|
96
|
-
else
|
97
|
-
@ss.tokenize o.value
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def visit_Psych_Nodes_Mapping_with_class(object)
|
36
|
+
def visit_Psych_Nodes_Mapping_with_class(object) # rubocop:disable CyclomaticComplexity, MethodName
|
102
37
|
return revive(Psych.load_tags[object.tag], object) if Psych.load_tags[object.tag]
|
103
38
|
|
104
39
|
case object.tag
|
105
40
|
when /^!ruby\/ActiveRecord:(.+)$/
|
106
|
-
klass = resolve_class(
|
41
|
+
klass = resolve_class(Regexp.last_match[1])
|
107
42
|
payload = Hash[*object.children.map { |c| accept c }]
|
108
|
-
id = payload[
|
43
|
+
id = payload['attributes'][klass.primary_key]
|
109
44
|
begin
|
110
45
|
klass.unscoped.find(id)
|
111
|
-
rescue ActiveRecord::RecordNotFound
|
112
|
-
raise Delayed::DeserializationError
|
46
|
+
rescue ActiveRecord::RecordNotFound => error
|
47
|
+
raise Delayed::DeserializationError, "ActiveRecord::RecordNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
|
113
48
|
end
|
114
49
|
when /^!ruby\/Mongoid:(.+)$/
|
115
|
-
klass = resolve_class(
|
50
|
+
klass = resolve_class(Regexp.last_match[1])
|
116
51
|
payload = Hash[*object.children.map { |c| accept c }]
|
52
|
+
id = payload['attributes']['_id']
|
117
53
|
begin
|
118
|
-
klass.find(
|
119
|
-
rescue Mongoid::Errors::DocumentNotFound
|
120
|
-
raise Delayed::DeserializationError
|
54
|
+
klass.find(id)
|
55
|
+
rescue Mongoid::Errors::DocumentNotFound => error
|
56
|
+
raise Delayed::DeserializationError, "Mongoid::Errors::DocumentNotFound, class: #{klass}, primary key: #{id} (#{error.message})"
|
121
57
|
end
|
122
58
|
when /^!ruby\/DataMapper:(.+)$/
|
123
|
-
klass = resolve_class(
|
59
|
+
klass = resolve_class(Regexp.last_match[1])
|
124
60
|
payload = Hash[*object.children.map { |c| accept c }]
|
125
61
|
begin
|
126
|
-
primary_keys = klass.properties.select
|
62
|
+
primary_keys = klass.properties.select(&:key?)
|
127
63
|
key_names = primary_keys.map { |p| p.name.to_s }
|
128
|
-
klass.get!(*key_names.map { |k| payload[
|
129
|
-
rescue DataMapper::ObjectNotFoundError
|
130
|
-
raise Delayed::DeserializationError
|
64
|
+
klass.get!(*key_names.map { |k| payload['attributes'][k] })
|
65
|
+
rescue DataMapper::ObjectNotFoundError => error
|
66
|
+
raise Delayed::DeserializationError, "DataMapper::ObjectNotFoundError, class: #{klass} (#{error.message})"
|
131
67
|
end
|
132
68
|
else
|
133
69
|
visit_Psych_Nodes_Mapping_without_class(object)
|