delayed_job_with_server_id 1.8.5
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/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +127 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/contrib/delayed_job.monitrc +14 -0
- data/delayed_job.gemspec +67 -0
- data/generators/delayed_job/delayed_job_generator.rb +22 -0
- data/generators/delayed_job/templates/migration.rb +21 -0
- data/generators/delayed_job/templates/script +5 -0
- data/init.rb +1 -0
- data/lib/delayed/command.rb +79 -0
- data/lib/delayed/job.rb +321 -0
- data/lib/delayed/message_sending.rb +22 -0
- data/lib/delayed/performable_method.rb +55 -0
- data/lib/delayed/recipes.rb +31 -0
- data/lib/delayed/tasks.rb +15 -0
- data/lib/delayed/worker.rb +57 -0
- data/lib/delayed_job.rb +13 -0
- data/recipes/delayed_job.rb +1 -0
- data/spec/database.rb +44 -0
- data/spec/delayed_method_spec.rb +150 -0
- data/spec/job_spec.rb +446 -0
- data/spec/story_spec.rb +17 -0
- data/tasks/jobs.rake +1 -0
- metadata +97 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Delayed
|
2
|
+
module MessageSending
|
3
|
+
def send_later(method, *args)
|
4
|
+
Delayed::Job.enqueue Delayed::PerformableMethod.new(self, method.to_sym, args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def send_at(time, method, *args)
|
8
|
+
Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args), 0, time)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def handle_asynchronously(method)
|
13
|
+
aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
|
14
|
+
with_method, without_method = "#{aliased_method}_with_send_later#{punctuation}", "#{aliased_method}_without_send_later#{punctuation}"
|
15
|
+
define_method(with_method) do |*args|
|
16
|
+
send_later(without_method, *args)
|
17
|
+
end
|
18
|
+
alias_method_chain method, :send_later
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Delayed
|
2
|
+
class PerformableMethod < Struct.new(:object, :method, :args)
|
3
|
+
CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
|
4
|
+
AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
|
5
|
+
|
6
|
+
def initialize(object, method, args)
|
7
|
+
raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method)
|
8
|
+
|
9
|
+
self.object = dump(object)
|
10
|
+
self.args = args.map { |a| dump(a) }
|
11
|
+
self.method = method.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def display_name
|
15
|
+
case self.object
|
16
|
+
when CLASS_STRING_FORMAT then "#{$1}.#{method}"
|
17
|
+
when AR_STRING_FORMAT then "#{$1}##{method}"
|
18
|
+
else "Unknown##{method}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
load(object).send(method, *args.map{|a| load(a)})
|
24
|
+
rescue ActiveRecord::RecordNotFound
|
25
|
+
# We cannot do anything about objects which were deleted in the meantime
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load(arg)
|
32
|
+
case arg
|
33
|
+
when CLASS_STRING_FORMAT then $1.constantize
|
34
|
+
when AR_STRING_FORMAT then $1.constantize.find($2)
|
35
|
+
else arg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump(arg)
|
40
|
+
case arg
|
41
|
+
when Class then class_to_string(arg)
|
42
|
+
when ActiveRecord::Base then ar_to_string(arg)
|
43
|
+
else arg
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ar_to_string(obj)
|
48
|
+
"AR:#{obj.class}:#{obj.id}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def class_to_string(obj)
|
52
|
+
"CLASS:#{obj.name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Capistrano Recipes for managing delayed_job
|
2
|
+
#
|
3
|
+
# Add these callbacks to have the delayed_job process restart when the server
|
4
|
+
# is restarted:
|
5
|
+
#
|
6
|
+
# after "deploy:stop", "delayed_job:stop"
|
7
|
+
# after "deploy:start", "delayed_job:start"
|
8
|
+
# after "deploy:restart", "delayed_job:restart"
|
9
|
+
|
10
|
+
Capistrano::Configuration.instance.load do
|
11
|
+
namespace :delayed_job do
|
12
|
+
def rails_env
|
13
|
+
fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Stop the delayed_job process"
|
17
|
+
task :stop, :roles => :app do
|
18
|
+
run "cd #{current_path};#{rails_env} script/delayed_job stop"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Start the delayed_job process"
|
22
|
+
task :start, :roles => :app do
|
23
|
+
run "cd #{current_path};#{rails_env} script/delayed_job start"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Restart the delayed_job process"
|
27
|
+
task :restart, :roles => :app do
|
28
|
+
run "cd #{current_path};#{rails_env} script/delayed_job restart"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Re-definitions are appended to existing tasks
|
2
|
+
task :environment
|
3
|
+
task :merb_env
|
4
|
+
|
5
|
+
namespace :jobs do
|
6
|
+
desc "Clear the delayed_job queue."
|
7
|
+
task :clear => [:merb_env, :environment] do
|
8
|
+
Delayed::Job.delete_all
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Start a delayed_job worker. "
|
12
|
+
task :work => [:merb_env, :environment] do
|
13
|
+
Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY'], :server_id => ENV['SERVER_ID']).start
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Delayed
|
2
|
+
class Worker
|
3
|
+
@@sleep_delay = 5
|
4
|
+
|
5
|
+
cattr_accessor :sleep_delay
|
6
|
+
|
7
|
+
cattr_accessor :logger
|
8
|
+
self.logger = if defined?(Merb::Logger)
|
9
|
+
Merb.logger
|
10
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
11
|
+
RAILS_DEFAULT_LOGGER
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
@quiet = options[:quiet]
|
16
|
+
Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
|
17
|
+
Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
|
18
|
+
Delayed::Job.server_id = options[:server_id] if options.has_key?(:server_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
say "*** Starting job worker #{Delayed::Job.worker_name}"
|
23
|
+
|
24
|
+
trap('TERM') { say 'Exiting...'; $exit = true }
|
25
|
+
trap('INT') { say 'Exiting...'; $exit = true }
|
26
|
+
|
27
|
+
loop do
|
28
|
+
result = nil
|
29
|
+
|
30
|
+
realtime = Benchmark.realtime do
|
31
|
+
result = Delayed::Job.work_off
|
32
|
+
end
|
33
|
+
|
34
|
+
count = result.sum
|
35
|
+
|
36
|
+
break if $exit
|
37
|
+
|
38
|
+
if count.zero?
|
39
|
+
sleep(@@sleep_delay)
|
40
|
+
else
|
41
|
+
say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
|
42
|
+
end
|
43
|
+
|
44
|
+
break if $exit
|
45
|
+
end
|
46
|
+
|
47
|
+
ensure
|
48
|
+
Delayed::Job.clear_locks!
|
49
|
+
end
|
50
|
+
|
51
|
+
def say(text)
|
52
|
+
puts text unless @quiet
|
53
|
+
logger.info text if logger
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/lib/delayed_job.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
autoload :ActiveRecord, 'activerecord'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/delayed/message_sending'
|
4
|
+
require File.dirname(__FILE__) + '/delayed/performable_method'
|
5
|
+
require File.dirname(__FILE__) + '/delayed/job'
|
6
|
+
require File.dirname(__FILE__) + '/delayed/worker'
|
7
|
+
|
8
|
+
Object.send(:include, Delayed::MessageSending)
|
9
|
+
Module.send(:include, Delayed::MessageSending::ClassMethods)
|
10
|
+
|
11
|
+
if defined?(Merb::Plugins)
|
12
|
+
Merb::Plugins.add_rakefiles File.dirname(__FILE__) / 'delayed' / 'tasks'
|
13
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
|
data/spec/database.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
$:.unshift(File.dirname(__FILE__) + '/../../rspec/lib')
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'active_record'
|
6
|
+
gem 'sqlite3-ruby'
|
7
|
+
|
8
|
+
require File.dirname(__FILE__) + '/../init'
|
9
|
+
require 'spec'
|
10
|
+
|
11
|
+
ActiveRecord::Base.logger = Logger.new('/tmp/dj.log')
|
12
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => '/tmp/jobs.sqlite')
|
13
|
+
ActiveRecord::Migration.verbose = false
|
14
|
+
ActiveRecord::Base.default_timezone = :utc if Time.zone.nil?
|
15
|
+
|
16
|
+
ActiveRecord::Schema.define do
|
17
|
+
|
18
|
+
create_table :delayed_jobs, :force => true do |table|
|
19
|
+
table.integer :priority, :default => 0
|
20
|
+
table.integer :attempts, :default => 0
|
21
|
+
table.text :handler
|
22
|
+
table.string :last_error
|
23
|
+
table.datetime :run_at
|
24
|
+
table.datetime :locked_at
|
25
|
+
table.string :locked_by
|
26
|
+
table.datetime :failed_at
|
27
|
+
table.string :server
|
28
|
+
table.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
create_table :stories, :force => true do |table|
|
32
|
+
table.string :text
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Purely useful for test cases...
|
39
|
+
class Story < ActiveRecord::Base
|
40
|
+
def tell; text; end
|
41
|
+
def whatever(n, _); tell*n; end
|
42
|
+
|
43
|
+
handle_asynchronously :whatever
|
44
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/database'
|
2
|
+
|
3
|
+
class SimpleJob
|
4
|
+
cattr_accessor :runs; self.runs = 0
|
5
|
+
def perform; @@runs += 1; end
|
6
|
+
end
|
7
|
+
|
8
|
+
class RandomRubyObject
|
9
|
+
def say_hello
|
10
|
+
'hello'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ErrorObject
|
15
|
+
|
16
|
+
def throw
|
17
|
+
raise ActiveRecord::RecordNotFound, '...'
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class StoryReader
|
24
|
+
|
25
|
+
def read(story)
|
26
|
+
"Epilog: #{story.tell}"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
class StoryReader
|
32
|
+
|
33
|
+
def read(story)
|
34
|
+
"Epilog: #{story.tell}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'random ruby objects' do
|
40
|
+
before { Delayed::Job.delete_all }
|
41
|
+
|
42
|
+
it "should respond_to :send_later method" do
|
43
|
+
|
44
|
+
RandomRubyObject.new.respond_to?(:send_later)
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should raise a ArgumentError if send_later is called but the target method doesn't exist" do
|
49
|
+
lambda { RandomRubyObject.new.send_later(:method_that_deos_not_exist) }.should raise_error(NoMethodError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should add a new entry to the job table when send_later is called on it" do
|
53
|
+
Delayed::Job.count.should == 0
|
54
|
+
|
55
|
+
RandomRubyObject.new.send_later(:to_s)
|
56
|
+
|
57
|
+
Delayed::Job.count.should == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should add a new entry to the job table when send_later is called on the class" do
|
61
|
+
Delayed::Job.count.should == 0
|
62
|
+
|
63
|
+
RandomRubyObject.send_later(:to_s)
|
64
|
+
|
65
|
+
Delayed::Job.count.should == 1
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should run get the original method executed when the job is performed" do
|
69
|
+
|
70
|
+
RandomRubyObject.new.send_later(:say_hello)
|
71
|
+
|
72
|
+
Delayed::Job.count.should == 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should ignore ActiveRecord::RecordNotFound errors because they are permanent" do
|
76
|
+
|
77
|
+
ErrorObject.new.send_later(:throw)
|
78
|
+
|
79
|
+
Delayed::Job.count.should == 1
|
80
|
+
|
81
|
+
Delayed::Job.reserve_and_run_one_job
|
82
|
+
|
83
|
+
Delayed::Job.count.should == 0
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should store the object as string if its an active record" do
|
88
|
+
story = Story.create :text => 'Once upon...'
|
89
|
+
story.send_later(:tell)
|
90
|
+
|
91
|
+
job = Delayed::Job.find(:first)
|
92
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
93
|
+
job.payload_object.object.should == "AR:Story:#{story.id}"
|
94
|
+
job.payload_object.method.should == :tell
|
95
|
+
job.payload_object.args.should == []
|
96
|
+
job.payload_object.perform.should == 'Once upon...'
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should store arguments as string if they an active record" do
|
100
|
+
|
101
|
+
story = Story.create :text => 'Once upon...'
|
102
|
+
|
103
|
+
reader = StoryReader.new
|
104
|
+
reader.send_later(:read, story)
|
105
|
+
|
106
|
+
job = Delayed::Job.find(:first)
|
107
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
108
|
+
job.payload_object.method.should == :read
|
109
|
+
job.payload_object.args.should == ["AR:Story:#{story.id}"]
|
110
|
+
job.payload_object.perform.should == 'Epilog: Once upon...'
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should call send later on methods which are wrapped with handle_asynchronously" do
|
114
|
+
story = Story.create :text => 'Once upon...'
|
115
|
+
|
116
|
+
Delayed::Job.count.should == 0
|
117
|
+
|
118
|
+
story.whatever(1, 5)
|
119
|
+
|
120
|
+
Delayed::Job.count.should == 1
|
121
|
+
job = Delayed::Job.find(:first)
|
122
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
123
|
+
job.payload_object.method.should == :whatever_without_send_later
|
124
|
+
job.payload_object.args.should == [1, 5]
|
125
|
+
job.payload_object.perform.should == 'Once upon...'
|
126
|
+
end
|
127
|
+
|
128
|
+
context "send_at" do
|
129
|
+
it "should queue a new job" do
|
130
|
+
lambda do
|
131
|
+
"string".send_at(1.hour.from_now, :length)
|
132
|
+
end.should change { Delayed::Job.count }.by(1)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should schedule the job in the future" do
|
136
|
+
time = 1.hour.from_now
|
137
|
+
job = "string".send_at(time, :length)
|
138
|
+
job.run_at.should == time
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should store payload as PerformableMethod" do
|
142
|
+
job = "string".send_at(1.hour.from_now, :count, 'r')
|
143
|
+
job.payload_object.class.should == Delayed::PerformableMethod
|
144
|
+
job.payload_object.method.should == :count
|
145
|
+
job.payload_object.args.should == ['r']
|
146
|
+
job.payload_object.perform.should == 1
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
data/spec/job_spec.rb
ADDED
@@ -0,0 +1,446 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/database'
|
2
|
+
|
3
|
+
class SimpleJob
|
4
|
+
cattr_accessor :runs; self.runs = 0
|
5
|
+
def perform; @@runs += 1; end
|
6
|
+
end
|
7
|
+
|
8
|
+
class ErrorJob
|
9
|
+
cattr_accessor :runs; self.runs = 0
|
10
|
+
def perform; raise 'did not work'; end
|
11
|
+
end
|
12
|
+
|
13
|
+
class LongRunningJob
|
14
|
+
def perform; sleep 250; end
|
15
|
+
end
|
16
|
+
|
17
|
+
module M
|
18
|
+
class ModuleJob
|
19
|
+
cattr_accessor :runs; self.runs = 0
|
20
|
+
def perform; @@runs += 1; end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe Delayed::Job do
|
26
|
+
before do
|
27
|
+
Delayed::Job.max_priority = nil
|
28
|
+
Delayed::Job.min_priority = nil
|
29
|
+
Delayed::Job.server_id = nil
|
30
|
+
|
31
|
+
Delayed::Job.delete_all
|
32
|
+
end
|
33
|
+
|
34
|
+
before(:each) do
|
35
|
+
SimpleJob.runs = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should set run_at automatically if not set" do
|
39
|
+
Delayed::Job.create(:payload_object => ErrorJob.new ).run_at.should_not == nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not set run_at automatically if already set" do
|
43
|
+
later = 5.minutes.from_now
|
44
|
+
Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should == later
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should raise ArgumentError when handler doesn't respond_to :perform" do
|
48
|
+
lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should increase count after enqueuing items" do
|
52
|
+
Delayed::Job.enqueue SimpleJob.new
|
53
|
+
Delayed::Job.count.should == 1
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should be able to set priority when enqueuing items" do
|
57
|
+
Delayed::Job.enqueue SimpleJob.new, 5
|
58
|
+
Delayed::Job.first.priority.should == 5
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should be able to set run_at when enqueuing items" do
|
62
|
+
later = (Delayed::Job.db_time_now+5.minutes)
|
63
|
+
Delayed::Job.enqueue SimpleJob.new, 5, later
|
64
|
+
|
65
|
+
# use be close rather than equal to because millisecond values cn be lost in DB round trip
|
66
|
+
Delayed::Job.first.run_at.should be_close(later, 1)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should be able to set server affinity when enqueuing items" do
|
70
|
+
Delayed::Job.enqueue SimpleJob.new, nil, nil, 1
|
71
|
+
Delayed::Job.first.server.should == 1
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should call perform on jobs when running work_off" do
|
75
|
+
SimpleJob.runs.should == 0
|
76
|
+
|
77
|
+
Delayed::Job.enqueue SimpleJob.new
|
78
|
+
Delayed::Job.work_off
|
79
|
+
|
80
|
+
SimpleJob.runs.should == 1
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
it "should work with eval jobs" do
|
85
|
+
$eval_job_ran = false
|
86
|
+
|
87
|
+
Delayed::Job.enqueue do <<-JOB
|
88
|
+
$eval_job_ran = true
|
89
|
+
JOB
|
90
|
+
end
|
91
|
+
|
92
|
+
Delayed::Job.work_off
|
93
|
+
|
94
|
+
$eval_job_ran.should == true
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should work with jobs in modules" do
|
98
|
+
M::ModuleJob.runs.should == 0
|
99
|
+
|
100
|
+
Delayed::Job.enqueue M::ModuleJob.new
|
101
|
+
Delayed::Job.work_off
|
102
|
+
|
103
|
+
M::ModuleJob.runs.should == 1
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should re-schedule by about 1 second at first and increment this more and more minutes when it fails to execute properly" do
|
107
|
+
Delayed::Job.enqueue ErrorJob.new
|
108
|
+
Delayed::Job.work_off(1)
|
109
|
+
|
110
|
+
job = Delayed::Job.find(:first)
|
111
|
+
|
112
|
+
job.last_error.should =~ /did not work/
|
113
|
+
job.last_error.should =~ /job_spec.rb:10:in `perform'/
|
114
|
+
job.attempts.should == 1
|
115
|
+
|
116
|
+
job.run_at.should > Delayed::Job.db_time_now - 10.minutes
|
117
|
+
job.run_at.should < Delayed::Job.db_time_now + 10.minutes
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
|
121
|
+
Delayed::Job.destroy_failed_jobs = false
|
122
|
+
Delayed::Job::max_attempts = 1
|
123
|
+
job = Delayed::Job.enqueue ErrorJob.new
|
124
|
+
Delayed::Job.work_off
|
125
|
+
job.reload
|
126
|
+
job.last_error.should =~ /did not work/
|
127
|
+
job.last_error.should =~ /job_spec.rb/
|
128
|
+
job.attempts.should == 1
|
129
|
+
|
130
|
+
job.failed_at.should_not == nil
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise an DeserializationError when the job class is totally unknown" do
|
134
|
+
|
135
|
+
job = Delayed::Job.new
|
136
|
+
job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
|
137
|
+
|
138
|
+
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should try to load the class when it is unknown at the time of the deserialization" do
|
142
|
+
job = Delayed::Job.new
|
143
|
+
job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
|
144
|
+
|
145
|
+
job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
|
146
|
+
|
147
|
+
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should try include the namespace when loading unknown objects" do
|
151
|
+
job = Delayed::Job.new
|
152
|
+
job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
|
153
|
+
job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
|
154
|
+
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should also try to load structs when they are unknown (raises TypeError)" do
|
158
|
+
job = Delayed::Job.new
|
159
|
+
job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
|
160
|
+
|
161
|
+
job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
|
162
|
+
|
163
|
+
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should try include the namespace when loading unknown structs" do
|
167
|
+
job = Delayed::Job.new
|
168
|
+
job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
|
169
|
+
|
170
|
+
job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
|
171
|
+
lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
|
172
|
+
end
|
173
|
+
|
174
|
+
context "reschedule" do
|
175
|
+
before do
|
176
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new
|
177
|
+
end
|
178
|
+
|
179
|
+
context "and we want to destroy jobs" do
|
180
|
+
before do
|
181
|
+
Delayed::Job.destroy_failed_jobs = true
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should be destroyed if it failed more than Job::max_attempts times" do
|
185
|
+
@job.should_receive(:destroy)
|
186
|
+
Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should not be destroyed if failed fewer than Job::max_attempts times" do
|
190
|
+
@job.should_not_receive(:destroy)
|
191
|
+
(Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "and we don't want to destroy jobs" do
|
196
|
+
before do
|
197
|
+
Delayed::Job.destroy_failed_jobs = false
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should be failed if it failed more than Job::max_attempts times" do
|
201
|
+
@job.reload.failed_at.should == nil
|
202
|
+
Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
|
203
|
+
@job.reload.failed_at.should_not == nil
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should not be failed if it failed fewer than Job::max_attempts times" do
|
207
|
+
(Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
|
208
|
+
@job.reload.failed_at.should == nil
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should fail after Job::max_run_time" do
|
215
|
+
@job = Delayed::Job.create :payload_object => LongRunningJob.new
|
216
|
+
Delayed::Job.reserve_and_run_one_job(1.second)
|
217
|
+
@job.reload.last_error.should =~ /expired/
|
218
|
+
@job.attempts.should == 1
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should never find failed jobs" do
|
222
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Delayed::Job.db_time_now
|
223
|
+
Delayed::Job.find_available(1).length.should == 0
|
224
|
+
end
|
225
|
+
|
226
|
+
context "when another worker is already performing an task, it" do
|
227
|
+
|
228
|
+
before :each do
|
229
|
+
Delayed::Job.worker_name = 'worker1'
|
230
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
|
231
|
+
end
|
232
|
+
|
233
|
+
it "should not allow a second worker to get exclusive access" do
|
234
|
+
@job.lock_exclusively!(4.hours, 'worker2').should == false
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should allow a second worker to get exclusive access if the timeout has passed" do
|
238
|
+
@job.lock_exclusively!(1.minute, 'worker2').should == true
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should be able to get access to the task if it was started more then max_age ago" do
|
242
|
+
@job.locked_at = 5.hours.ago
|
243
|
+
@job.save
|
244
|
+
|
245
|
+
@job.lock_exclusively! 4.hours, 'worker2'
|
246
|
+
@job.reload
|
247
|
+
@job.locked_by.should == 'worker2'
|
248
|
+
@job.locked_at.should > 1.minute.ago
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should not be found by another worker" do
|
252
|
+
Delayed::Job.worker_name = 'worker2'
|
253
|
+
|
254
|
+
Delayed::Job.find_available(1, 6.minutes).length.should == 0
|
255
|
+
end
|
256
|
+
|
257
|
+
it "should be found by another worker if the time has expired" do
|
258
|
+
Delayed::Job.worker_name = 'worker2'
|
259
|
+
|
260
|
+
Delayed::Job.find_available(1, 4.minutes).length.should == 1
|
261
|
+
end
|
262
|
+
|
263
|
+
it "should be able to get exclusive access again when the worker name is the same" do
|
264
|
+
@job.lock_exclusively! 5.minutes, 'worker1'
|
265
|
+
@job.lock_exclusively! 5.minutes, 'worker1'
|
266
|
+
@job.lock_exclusively! 5.minutes, 'worker1'
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
context "when another worker has worked on a task since the job was found to be available, it" do
|
271
|
+
|
272
|
+
before :each do
|
273
|
+
Delayed::Job.worker_name = 'worker1'
|
274
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new
|
275
|
+
@job_copy_for_worker_2 = Delayed::Job.find(@job.id)
|
276
|
+
end
|
277
|
+
|
278
|
+
it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
|
279
|
+
@job.delete
|
280
|
+
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
|
281
|
+
end
|
282
|
+
|
283
|
+
it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
|
284
|
+
@job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
|
285
|
+
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context "#name" do
|
290
|
+
it "should be the class name of the job that was enqueued" do
|
291
|
+
Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
|
292
|
+
end
|
293
|
+
|
294
|
+
it "should be the method that will be called if its a performable method object" do
|
295
|
+
Delayed::Job.send_later(:clear_locks!)
|
296
|
+
Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
|
297
|
+
|
298
|
+
end
|
299
|
+
it "should be the instance method that will be called if its a performable method object" do
|
300
|
+
story = Story.create :text => "..."
|
301
|
+
|
302
|
+
story.send_later(:save)
|
303
|
+
|
304
|
+
Delayed::Job.last.name.should == 'Story#save'
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context "worker prioritization" do
|
309
|
+
|
310
|
+
before(:each) do
|
311
|
+
Delayed::Job.max_priority = nil
|
312
|
+
Delayed::Job.min_priority = nil
|
313
|
+
end
|
314
|
+
|
315
|
+
it "should only work_off jobs that are >= min_priority" do
|
316
|
+
Delayed::Job.min_priority = -5
|
317
|
+
Delayed::Job.max_priority = 5
|
318
|
+
SimpleJob.runs.should == 0
|
319
|
+
|
320
|
+
Delayed::Job.enqueue SimpleJob.new, -10
|
321
|
+
Delayed::Job.enqueue SimpleJob.new, 0
|
322
|
+
Delayed::Job.work_off
|
323
|
+
|
324
|
+
SimpleJob.runs.should == 1
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should only work_off jobs that are <= max_priority" do
|
328
|
+
Delayed::Job.min_priority = -5
|
329
|
+
Delayed::Job.max_priority = 5
|
330
|
+
SimpleJob.runs.should == 0
|
331
|
+
|
332
|
+
Delayed::Job.enqueue SimpleJob.new, 10
|
333
|
+
Delayed::Job.enqueue SimpleJob.new, 0
|
334
|
+
|
335
|
+
Delayed::Job.work_off
|
336
|
+
|
337
|
+
SimpleJob.runs.should == 1
|
338
|
+
end
|
339
|
+
|
340
|
+
it "should fetch jobs ordered by priority" do
|
341
|
+
number_of_jobs = 10
|
342
|
+
number_of_jobs.times { Delayed::Job.enqueue SimpleJob.new, rand(10) }
|
343
|
+
jobs = Delayed::Job.find_available(10)
|
344
|
+
ordered = true
|
345
|
+
jobs[1..-1].each_index{ |i|
|
346
|
+
if (jobs[i].priority < jobs[i+1].priority)
|
347
|
+
ordered = false
|
348
|
+
break
|
349
|
+
end
|
350
|
+
}
|
351
|
+
ordered.should == true
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
context "server affinity" do
|
357
|
+
|
358
|
+
before(:each) do
|
359
|
+
Delayed::Job.server_id = nil
|
360
|
+
end
|
361
|
+
|
362
|
+
it "should not work_off jobs that are assigned to another server" do
|
363
|
+
Delayed::Job.server_id = 1
|
364
|
+
SimpleJob.runs.should == 0
|
365
|
+
|
366
|
+
Delayed::Job.enqueue SimpleJob.new
|
367
|
+
Delayed::Job.enqueue SimpleJob.new, nil, nil, 1
|
368
|
+
Delayed::Job.enqueue SimpleJob.new, nil, nil, 2
|
369
|
+
|
370
|
+
Delayed::Job.work_off
|
371
|
+
|
372
|
+
SimpleJob.runs.should == 2
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
|
377
|
+
context "when pulling jobs off the queue for processing, it" do
|
378
|
+
before(:each) do
|
379
|
+
@job = Delayed::Job.create(
|
380
|
+
:payload_object => SimpleJob.new,
|
381
|
+
:locked_by => 'worker1',
|
382
|
+
:locked_at => Delayed::Job.db_time_now - 5.minutes)
|
383
|
+
end
|
384
|
+
|
385
|
+
it "should leave the queue in a consistent state and not run the job if locking fails" do
|
386
|
+
SimpleJob.runs.should == 0
|
387
|
+
@job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
|
388
|
+
Delayed::Job.should_receive(:find_available).once.and_return([@job])
|
389
|
+
Delayed::Job.work_off(1)
|
390
|
+
SimpleJob.runs.should == 0
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
context "while running alongside other workers that locked jobs, it" do
|
396
|
+
before(:each) do
|
397
|
+
Delayed::Job.worker_name = 'worker1'
|
398
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
399
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
400
|
+
Delayed::Job.create(:payload_object => SimpleJob.new)
|
401
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
402
|
+
end
|
403
|
+
|
404
|
+
it "should ingore locked jobs from other workers" do
|
405
|
+
Delayed::Job.worker_name = 'worker3'
|
406
|
+
SimpleJob.runs.should == 0
|
407
|
+
Delayed::Job.work_off
|
408
|
+
SimpleJob.runs.should == 1 # runs the one open job
|
409
|
+
end
|
410
|
+
|
411
|
+
it "should find our own jobs regardless of locks" do
|
412
|
+
Delayed::Job.worker_name = 'worker1'
|
413
|
+
SimpleJob.runs.should == 0
|
414
|
+
Delayed::Job.work_off
|
415
|
+
SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
context "while running with locked and expired jobs, it" do
|
420
|
+
before(:each) do
|
421
|
+
Delayed::Job.worker_name = 'worker1'
|
422
|
+
exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::max_run_time)
|
423
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
|
424
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
425
|
+
Delayed::Job.create(:payload_object => SimpleJob.new)
|
426
|
+
Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
|
427
|
+
end
|
428
|
+
|
429
|
+
it "should only find unlocked and expired jobs" do
|
430
|
+
Delayed::Job.worker_name = 'worker3'
|
431
|
+
SimpleJob.runs.should == 0
|
432
|
+
Delayed::Job.work_off
|
433
|
+
SimpleJob.runs.should == 2 # runs the one open job and one expired job
|
434
|
+
end
|
435
|
+
|
436
|
+
it "should ignore locks when finding our own jobs" do
|
437
|
+
Delayed::Job.worker_name = 'worker1'
|
438
|
+
SimpleJob.runs.should == 0
|
439
|
+
Delayed::Job.work_off
|
440
|
+
SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
|
441
|
+
# This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
|
442
|
+
end
|
443
|
+
|
444
|
+
end
|
445
|
+
|
446
|
+
end
|