delayed_job 2.1.0.pre2 → 2.1.1
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.
- data/README.textile +78 -43
- data/lib/delayed/backend/active_record.rb +14 -29
- data/lib/delayed/backend/base.rb +66 -37
- data/lib/delayed/backend/shared_spec.rb +188 -191
- data/lib/delayed/command.rb +3 -2
- data/lib/delayed/deserialization_error.rb +4 -0
- data/lib/delayed/message_sending.rb +19 -11
- data/lib/delayed/performable_mailer.rb +21 -0
- data/lib/delayed/performable_method.rb +19 -15
- data/lib/delayed/recipes.rb +6 -2
- data/lib/delayed/serialization/active_record.rb +13 -0
- data/lib/delayed/worker.rb +31 -40
- data/lib/delayed_job.rb +4 -2
- data/spec/active_record_job_spec.rb +4 -4
- data/spec/database.yml +4 -0
- data/spec/message_sending_spec.rb +42 -4
- data/spec/performable_mailer_spec.rb +46 -0
- data/spec/performable_method_spec.rb +27 -11
- data/spec/sample_jobs.rb +27 -19
- data/spec/spec_helper.rb +10 -8
- metadata +57 -21
- data/generators/delayed_job/delayed_job_generator.rb +0 -22
- data/generators/delayed_job/templates/migration.rb +0 -21
- data/generators/delayed_job/templates/script +0 -5
- data/init.rb +0 -1
- data/rails/init.rb +0 -5
- data/tasks/jobs.rake +0 -1
data/lib/delayed/command.rb
CHANGED
@@ -44,8 +44,9 @@ module Delayed
|
|
44
44
|
opts.on('-m', '--monitor', 'Start monitor process.') do
|
45
45
|
@monitor = true
|
46
46
|
end
|
47
|
-
|
48
|
-
|
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
|
49
50
|
end
|
50
51
|
@args = opts.parse!(args)
|
51
52
|
end
|
@@ -3,25 +3,23 @@ require 'active_support/core_ext/module/aliasing'
|
|
3
3
|
|
4
4
|
module Delayed
|
5
5
|
class DelayProxy < ActiveSupport::BasicObject
|
6
|
-
def initialize(target, options)
|
6
|
+
def initialize(payload_class, target, options)
|
7
|
+
@payload_class = payload_class
|
7
8
|
@target = target
|
8
9
|
@options = options
|
9
10
|
end
|
10
11
|
|
11
12
|
def method_missing(method, *args)
|
12
|
-
Job.
|
13
|
-
:payload_object => PerformableMethod.new(@target, method.to_sym, args),
|
14
|
-
:priority => ::Delayed::Worker.default_priority
|
15
|
-
}.merge(@options))
|
13
|
+
Job.enqueue({:payload_object => @payload_class.new(@target, method.to_sym, args)}.merge(@options))
|
16
14
|
end
|
17
15
|
end
|
18
16
|
|
19
17
|
module MessageSending
|
20
18
|
def delay(options = {})
|
21
|
-
DelayProxy.new(self, options)
|
19
|
+
DelayProxy.new(PerformableMethod, self, options)
|
22
20
|
end
|
23
21
|
alias __delay__ delay
|
24
|
-
|
22
|
+
|
25
23
|
def send_later(method, *args)
|
26
24
|
warn "[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method"
|
27
25
|
__delay__.__send__(method, *args)
|
@@ -31,16 +29,26 @@ module Delayed
|
|
31
29
|
warn "[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method"
|
32
30
|
__delay__(:run_at => time).__send__(method, *args)
|
33
31
|
end
|
34
|
-
|
32
|
+
|
35
33
|
module ClassMethods
|
36
|
-
def handle_asynchronously(method)
|
34
|
+
def handle_asynchronously(method, opts = {})
|
37
35
|
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
38
36
|
with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
|
39
37
|
define_method(with_method) do |*args|
|
40
|
-
|
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)
|
41
49
|
end
|
42
50
|
alias_method_chain method, :delay
|
43
51
|
end
|
44
52
|
end
|
45
|
-
end
|
53
|
+
end
|
46
54
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'action_mailer'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
class PerformableMailer < PerformableMethod
|
5
|
+
def perform
|
6
|
+
object.send(method_name, *args).deliver
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
ActionMailer::Base.class_eval do
|
12
|
+
def self.delay(options = {})
|
13
|
+
Delayed::DelayProxy.new(Delayed::PerformableMailer, self, options)
|
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
|
@@ -1,27 +1,31 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
1
3
|
module Delayed
|
2
|
-
class PerformableMethod < Struct.new(:object, :
|
3
|
-
|
4
|
-
|
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)
|
5
9
|
|
6
|
-
self.object
|
7
|
-
self.args
|
8
|
-
self.
|
10
|
+
self.object = object
|
11
|
+
self.args = args
|
12
|
+
self.method_name = method_name.to_sym
|
9
13
|
end
|
10
|
-
|
14
|
+
|
11
15
|
def display_name
|
12
|
-
"#{object.class}##{
|
16
|
+
"#{object.class}##{method_name}"
|
13
17
|
end
|
14
|
-
|
18
|
+
|
15
19
|
def perform
|
16
|
-
object.send(
|
20
|
+
object.send(method_name, *args) if object
|
17
21
|
end
|
18
|
-
|
22
|
+
|
19
23
|
def method_missing(symbol, *args)
|
20
|
-
object.
|
24
|
+
object.send(symbol, *args)
|
21
25
|
end
|
22
|
-
|
26
|
+
|
23
27
|
def respond_to?(symbol, include_private=false)
|
24
|
-
object.respond_to?(symbol, include_private)
|
25
|
-
end
|
28
|
+
super || object.respond_to?(symbol, include_private)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
end
|
data/lib/delayed/recipes.rb
CHANGED
@@ -13,6 +13,10 @@ Capistrano::Configuration.instance.load do
|
|
13
13
|
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
14
14
|
end
|
15
15
|
|
16
|
+
def args
|
17
|
+
fetch(:delayed_job_args, "")
|
18
|
+
end
|
19
|
+
|
16
20
|
desc "Stop the delayed_job process"
|
17
21
|
task :stop, :roles => :app do
|
18
22
|
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
@@ -20,12 +24,12 @@ Capistrano::Configuration.instance.load do
|
|
20
24
|
|
21
25
|
desc "Start the delayed_job process"
|
22
26
|
task :start, :roles => :app do
|
23
|
-
run "cd #{current_path};#{rails_env} script/delayed_job start"
|
27
|
+
run "cd #{current_path};#{rails_env} script/delayed_job start #{args}"
|
24
28
|
end
|
25
29
|
|
26
30
|
desc "Restart the delayed_job process"
|
27
31
|
task :restart, :roles => :app do
|
28
|
-
run "cd #{current_path};#{rails_env} script/delayed_job restart"
|
32
|
+
run "cd #{current_path};#{rails_env} script/delayed_job restart #{args}"
|
29
33
|
end
|
30
34
|
end
|
31
35
|
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
|
data/lib/delayed/worker.rb
CHANGED
@@ -11,12 +11,12 @@ module Delayed
|
|
11
11
|
self.max_attempts = 25
|
12
12
|
self.max_run_time = 4.hours
|
13
13
|
self.default_priority = 0
|
14
|
-
|
14
|
+
|
15
15
|
# By default failed jobs are destroyed after too many attempts. If you want to keep them around
|
16
16
|
# (perhaps to inspect the reason for the failure), set this to false.
|
17
17
|
cattr_accessor :destroy_failed_jobs
|
18
18
|
self.destroy_failed_jobs = true
|
19
|
-
|
19
|
+
|
20
20
|
self.logger = if defined?(Rails)
|
21
21
|
Rails.logger
|
22
22
|
elsif defined?(RAILS_DEFAULT_LOGGER)
|
@@ -25,26 +25,28 @@ module Delayed
|
|
25
25
|
|
26
26
|
# name_prefix is ignored if name is set directly
|
27
27
|
attr_accessor :name_prefix
|
28
|
-
|
28
|
+
|
29
29
|
cattr_reader :backend
|
30
|
-
|
30
|
+
|
31
31
|
def self.backend=(backend)
|
32
32
|
if backend.is_a? Symbol
|
33
|
+
require "delayed/serialization/#{backend}"
|
33
34
|
require "delayed/backend/#{backend}"
|
34
35
|
backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
|
35
36
|
end
|
36
37
|
@@backend = backend
|
37
38
|
silence_warnings { ::Delayed.const_set(:Job, backend) }
|
38
39
|
end
|
39
|
-
|
40
|
+
|
40
41
|
def self.guess_backend
|
41
42
|
self.backend ||= :active_record if defined?(ActiveRecord)
|
42
43
|
end
|
43
44
|
|
44
45
|
def initialize(options={})
|
45
|
-
@quiet = options[:quiet]
|
46
|
+
@quiet = options.has_key?(:quiet) ? options[:quiet] : true
|
46
47
|
self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
47
48
|
self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
|
49
|
+
self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
|
48
50
|
end
|
49
51
|
|
50
52
|
# Every worker has a unique name which by default is the pid of the process. There are some
|
@@ -80,7 +82,7 @@ module Delayed
|
|
80
82
|
break if $exit
|
81
83
|
|
82
84
|
if count.zero?
|
83
|
-
sleep(
|
85
|
+
sleep(self.class.sleep_delay)
|
84
86
|
else
|
85
87
|
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
86
88
|
end
|
@@ -91,7 +93,7 @@ module Delayed
|
|
91
93
|
ensure
|
92
94
|
Delayed::Job.clear_locks!(name)
|
93
95
|
end
|
94
|
-
|
96
|
+
|
95
97
|
# Do num jobs and return stats on success/failure.
|
96
98
|
# Exit early if interrupted.
|
97
99
|
def work_off(num = 100)
|
@@ -111,7 +113,7 @@ module Delayed
|
|
111
113
|
|
112
114
|
return [success, failure]
|
113
115
|
end
|
114
|
-
|
116
|
+
|
115
117
|
def run(job)
|
116
118
|
runtime = Benchmark.realtime do
|
117
119
|
Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
|
@@ -119,34 +121,34 @@ module Delayed
|
|
119
121
|
end
|
120
122
|
say "#{job.name} completed after %.4f" % runtime
|
121
123
|
return true # did work
|
122
|
-
rescue
|
123
|
-
|
124
|
+
rescue DeserializationError => error
|
125
|
+
job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
|
126
|
+
failed(job)
|
127
|
+
rescue Exception => error
|
128
|
+
handle_failed_job(job, error)
|
124
129
|
return false # work failed
|
125
130
|
end
|
126
|
-
|
131
|
+
|
127
132
|
# Reschedule the job in the future (when a job fails).
|
128
133
|
# Uses an exponential scale depending on the number of failed attempts.
|
129
134
|
def reschedule(job, time = nil)
|
130
135
|
if (job.attempts += 1) < self.class.max_attempts
|
131
|
-
time ||=
|
136
|
+
time ||= job.reschedule_at
|
132
137
|
job.run_at = time
|
133
138
|
job.unlock
|
134
139
|
job.save!
|
135
140
|
else
|
136
141
|
say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
|
142
|
+
failed(job)
|
143
|
+
end
|
144
|
+
end
|
137
145
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
failure_method.call(job)
|
143
|
-
else
|
144
|
-
failure_method.call
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
|
146
|
+
def failed(job)
|
147
|
+
job.hook(:failure)
|
148
|
+
if job.respond_to?(:on_permanent_failure)
|
149
|
+
warn "[DEPRECATION] The #on_permanent_failure hook has been renamed to #failure."
|
149
150
|
end
|
151
|
+
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
|
150
152
|
end
|
151
153
|
|
152
154
|
def say(text, level = Logger::INFO)
|
@@ -156,30 +158,19 @@ module Delayed
|
|
156
158
|
end
|
157
159
|
|
158
160
|
protected
|
159
|
-
|
161
|
+
|
160
162
|
def handle_failed_job(job, error)
|
161
|
-
job.last_error = error.message
|
163
|
+
job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
|
162
164
|
say "#{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
|
163
165
|
reschedule(job)
|
164
166
|
end
|
165
|
-
|
167
|
+
|
166
168
|
# Run the next job we can get an exclusive lock on.
|
167
169
|
# If no jobs are left we return nil
|
168
170
|
def reserve_and_run_one_job
|
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
|
-
job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
|
173
|
-
if job.lock_exclusively!(self.class.max_run_time, name)
|
174
|
-
say "acquired lock on #{job.name}"
|
175
|
-
true
|
176
|
-
else
|
177
|
-
say "failed to acquire exclusive lock for #{job.name}", Logger::WARN
|
178
|
-
false
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
171
|
+
job = Delayed::Job.reserve(self)
|
182
172
|
run(job) if job
|
183
173
|
end
|
184
174
|
end
|
175
|
+
|
185
176
|
end
|
data/lib/delayed_job.rb
CHANGED
@@ -2,10 +2,12 @@ require 'active_support'
|
|
2
2
|
|
3
3
|
require File.dirname(__FILE__) + '/delayed/message_sending'
|
4
4
|
require File.dirname(__FILE__) + '/delayed/performable_method'
|
5
|
+
require File.dirname(__FILE__) + '/delayed/performable_mailer' if defined?(ActionMailer)
|
5
6
|
require File.dirname(__FILE__) + '/delayed/yaml_ext'
|
6
7
|
require File.dirname(__FILE__) + '/delayed/backend/base'
|
7
8
|
require File.dirname(__FILE__) + '/delayed/worker'
|
8
|
-
require File.dirname(__FILE__) + '/delayed/
|
9
|
+
require File.dirname(__FILE__) + '/delayed/deserialization_error'
|
10
|
+
require File.dirname(__FILE__) + '/delayed/railtie' if defined?(Rails::Railtie)
|
9
11
|
|
10
|
-
Object.send(:include, Delayed::MessageSending)
|
12
|
+
Object.send(:include, Delayed::MessageSending)
|
11
13
|
Module.send(:include, Delayed::MessageSending::ClassMethods)
|
@@ -5,7 +5,7 @@ describe Delayed::Backend::ActiveRecord::Job do
|
|
5
5
|
after do
|
6
6
|
Time.zone = nil
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
it_should_behave_like 'a delayed_job backend'
|
10
10
|
|
11
11
|
context "db_time_now" do
|
@@ -13,7 +13,7 @@ describe Delayed::Backend::ActiveRecord::Job do
|
|
13
13
|
Time.zone = 'Eastern Time (US & Canada)'
|
14
14
|
%w(EST EDT).should include(Delayed::Job.db_time_now.zone)
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should return UTC time if that is the AR default" do
|
18
18
|
Time.zone = nil
|
19
19
|
ActiveRecord::Base.default_timezone = :utc
|
@@ -26,10 +26,10 @@ describe Delayed::Backend::ActiveRecord::Job do
|
|
26
26
|
%w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
|
27
27
|
end
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
describe "after_fork" do
|
31
31
|
it "should call reconnect on the connection" do
|
32
|
-
ActiveRecord::Base.
|
32
|
+
ActiveRecord::Base.should_receive(:establish_connection)
|
33
33
|
Delayed::Backend::ActiveRecord::Job.after_fork
|
34
34
|
end
|
35
35
|
end
|
data/spec/database.yml
ADDED
@@ -7,21 +7,59 @@ describe Delayed::MessageSending do
|
|
7
7
|
end
|
8
8
|
handle_asynchronously :tell!
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
it "should alias original method" do
|
12
12
|
Story.new.should respond_to(:tell_without_delay!)
|
13
13
|
Story.new.should respond_to(:tell_with_delay!)
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
it "should create a PerformableMethod" do
|
17
17
|
story = Story.create!
|
18
18
|
lambda {
|
19
19
|
job = story.tell!(1)
|
20
20
|
job.payload_object.class.should == Delayed::PerformableMethod
|
21
|
-
job.payload_object.
|
21
|
+
job.payload_object.method_name.should == :tell_without_delay!
|
22
22
|
job.payload_object.args.should == [1]
|
23
23
|
}.should change { Delayed::Job.count }
|
24
24
|
end
|
25
|
+
|
26
|
+
describe 'with options' do
|
27
|
+
class Fable
|
28
|
+
class << self
|
29
|
+
attr_accessor :importance
|
30
|
+
end
|
31
|
+
def tell
|
32
|
+
end
|
33
|
+
handle_asynchronously :tell, :priority => Proc.new { self.importance }
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should set the priority based on the Fable importance' do
|
37
|
+
Fable.importance = 10
|
38
|
+
job = Fable.new.tell
|
39
|
+
job.priority.should == 10
|
40
|
+
|
41
|
+
Fable.importance = 20
|
42
|
+
job = Fable.new.tell
|
43
|
+
job.priority.should == 20
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'using a proc with parament' do
|
47
|
+
class Yarn
|
48
|
+
attr_accessor :importance
|
49
|
+
def spin
|
50
|
+
end
|
51
|
+
handle_asynchronously :spin, :priority => Proc.new {|y| y.importance }
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should set the priority based on the Fable importance' do
|
55
|
+
job = Yarn.new.tap {|y| y.importance = 10 }.spin
|
56
|
+
job.priority.should == 10
|
57
|
+
|
58
|
+
job = Yarn.new.tap {|y| y.importance = 20 }.spin
|
59
|
+
job.priority.should == 20
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
25
63
|
end
|
26
64
|
|
27
65
|
context "delay" do
|
@@ -29,7 +67,7 @@ describe Delayed::MessageSending do
|
|
29
67
|
lambda {
|
30
68
|
job = "hello".delay.count('l')
|
31
69
|
job.payload_object.class.should == Delayed::PerformableMethod
|
32
|
-
job.payload_object.
|
70
|
+
job.payload_object.method_name.should == :count
|
33
71
|
job.payload_object.args.should == ['l']
|
34
72
|
}.should change { Delayed::Job.count }.by(1)
|
35
73
|
end
|