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.
@@ -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
@@ -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