delayed_job 2.1.0.pre2 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|