delayed_job 1.8.5 → 1.9.0pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,6 +36,8 @@ module Delayed
36
36
  end
37
37
 
38
38
  def daemonize
39
+ Delayed::Worker.backend.before_fork
40
+
39
41
  ObjectSpace.each_object(File) do |file|
40
42
  @files_to_reopen << file unless file.closed?
41
43
  end
@@ -64,11 +66,11 @@ module Delayed
64
66
  if Delayed::Worker.logger.respond_to? :auto_flushing=
65
67
  Delayed::Worker.logger.auto_flushing = true
66
68
  end
67
- ActiveRecord::Base.connection.reconnect!
68
-
69
- Delayed::Job.worker_name = "#{worker_name} #{Delayed::Job.worker_name}"
69
+ Delayed::Worker.backend.after_fork
70
70
 
71
- Delayed::Worker.new(@options).start
71
+ worker = Delayed::Worker.new(@options)
72
+ worker.name_prefix = "#{worker_name} "
73
+ worker.start
72
74
  rescue => e
73
75
  Rails.logger.fatal e
74
76
  STDERR.puts e.message
@@ -1,7 +1,19 @@
1
+ class Class
2
+ def load_for_delayed_job(arg)
3
+ self
4
+ end
5
+
6
+ def dump_for_delayed_job
7
+ name
8
+ end
9
+ end
10
+
1
11
  module Delayed
2
12
  class PerformableMethod < Struct.new(:object, :method, :args)
3
- CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
4
- AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
13
+ STRING_FORMAT = /^LOAD\;([A-Z][\w\:]+)(?:\;(\w+))?$/
14
+
15
+ class LoadError < StandardError
16
+ end
5
17
 
6
18
  def initialize(object, method, args)
7
19
  raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method)
@@ -11,45 +23,40 @@ module Delayed
11
23
  self.method = method.to_sym
12
24
  end
13
25
 
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
-
26
+ def display_name
27
+ if STRING_FORMAT === object
28
+ "#{$1}#{$2 ? '#' : '.'}#{method}"
29
+ else
30
+ "#{object.class}##{method}"
31
+ end
32
+ end
33
+
22
34
  def perform
23
35
  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
36
+ rescue PerformableMethod::LoadError
37
+ # We cannot do anything about objects that can't be loaded
26
38
  true
27
39
  end
28
40
 
29
41
  private
30
42
 
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
43
+ def load(obj)
44
+ if STRING_FORMAT === obj
45
+ $1.constantize.load_for_delayed_job($2)
46
+ else
47
+ obj
36
48
  end
49
+ rescue => e
50
+ Delayed::Worker.logger.warn "Could not load object for job: #{e.message}"
51
+ raise PerformableMethod::LoadError
37
52
  end
38
53
 
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
54
+ def dump(obj)
55
+ if obj.respond_to?(:dump_for_delayed_job)
56
+ "LOAD;#{obj.dump_for_delayed_job}"
57
+ else
58
+ obj
44
59
  end
45
60
  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
61
  end
55
- end
62
+ end
@@ -1,24 +1,61 @@
1
+ require 'timeout'
2
+ require 'active_support/core_ext/numeric/time'
3
+
1
4
  module Delayed
2
5
  class Worker
3
- @@sleep_delay = 5
6
+ cattr_accessor :min_priority, :max_priority, :max_attempts, :max_run_time, :sleep_delay, :logger
7
+ self.sleep_delay = 5
8
+ self.max_attempts = 25
9
+ self.max_run_time = 4.hours
10
+
11
+ # By default failed jobs are destroyed after too many attempts. If you want to keep them around
12
+ # (perhaps to inspect the reason for the failure), set this to false.
13
+ cattr_accessor :destroy_failed_jobs
14
+ self.destroy_failed_jobs = true
4
15
 
5
- cattr_accessor :sleep_delay
6
-
7
- cattr_accessor :logger
8
16
  self.logger = if defined?(Merb::Logger)
9
17
  Merb.logger
10
18
  elsif defined?(RAILS_DEFAULT_LOGGER)
11
19
  RAILS_DEFAULT_LOGGER
12
20
  end
13
21
 
22
+ # name_prefix is ignored if name is set directly
23
+ attr_accessor :name_prefix
24
+
25
+ cattr_reader :backend
26
+
27
+ def self.backend=(backend)
28
+ if backend.is_a? Symbol
29
+ require "delayed/backend/#{backend}"
30
+ backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
31
+ end
32
+ @@backend = backend
33
+ silence_warnings { ::Delayed.const_set(:Job, backend) }
34
+ end
35
+
14
36
  def initialize(options={})
15
37
  @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)
38
+ self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
39
+ self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
40
+ end
41
+
42
+ # Every worker has a unique name which by default is the pid of the process. There are some
43
+ # advantages to overriding this with something which survives worker retarts: Workers can#
44
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
45
+ # it crashed before.
46
+ def name
47
+ return @name unless @name.nil?
48
+ "#{@name_prefix}host:#{Socket.gethostname} pid:#{Process.pid}" rescue "#{@name_prefix}pid:#{Process.pid}"
49
+ end
50
+
51
+ # Sets the name of the worker.
52
+ # Setting the name to nil will reset the default worker name
53
+ def name=(val)
54
+ @name = val
18
55
  end
19
56
 
20
57
  def start
21
- say "*** Starting job worker #{Delayed::Job.worker_name}"
58
+ say "*** Starting job worker #{name}"
22
59
 
23
60
  trap('TERM') { say 'Exiting...'; $exit = true }
24
61
  trap('INT') { say 'Exiting...'; $exit = true }
@@ -27,7 +64,7 @@ module Delayed
27
64
  result = nil
28
65
 
29
66
  realtime = Benchmark.realtime do
30
- result = Delayed::Job.work_off
67
+ result = work_off
31
68
  end
32
69
 
33
70
  count = result.sum
@@ -44,13 +81,86 @@ module Delayed
44
81
  end
45
82
 
46
83
  ensure
47
- Delayed::Job.clear_locks!
84
+ Delayed::Job.clear_locks!(name)
85
+ end
86
+
87
+ # Do num jobs and return stats on success/failure.
88
+ # Exit early if interrupted.
89
+ def work_off(num = 100)
90
+ success, failure = 0, 0
91
+
92
+ num.times do
93
+ case reserve_and_run_one_job
94
+ when true
95
+ success += 1
96
+ when false
97
+ failure += 1
98
+ else
99
+ break # leave if no work could be done
100
+ end
101
+ break if $exit # leave if we're exiting
102
+ end
103
+
104
+ return [success, failure]
105
+ end
106
+
107
+ def run(job)
108
+ runtime = Benchmark.realtime do
109
+ Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
110
+ job.destroy
111
+ end
112
+ # TODO: warn if runtime > max_run_time ?
113
+ say "* [JOB] #{name} completed after %.4f" % runtime
114
+ return true # did work
115
+ rescue Exception => e
116
+ handle_failed_job(job, e)
117
+ return false # work failed
118
+ end
119
+
120
+ # Reschedule the job in the future (when a job fails).
121
+ # Uses an exponential scale depending on the number of failed attempts.
122
+ def reschedule(job, time = nil)
123
+ if (job.attempts += 1) < self.class.max_attempts
124
+ time ||= Job.db_time_now + (job.attempts ** 4) + 5
125
+ job.run_at = time
126
+ job.unlock
127
+ job.save!
128
+ else
129
+ say "* [JOB] PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
130
+ self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
131
+ end
48
132
  end
49
133
 
50
- def say(text)
134
+ def say(text, level = Logger::INFO)
51
135
  puts text unless @quiet
52
- logger.info text if logger
136
+ logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
53
137
  end
54
138
 
139
+ protected
140
+
141
+ def handle_failed_job(job, error)
142
+ job.last_error = error.message + "\n" + error.backtrace.join("\n")
143
+ say "* [JOB] #{name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
144
+ reschedule(job)
145
+ end
146
+
147
+ # Run the next job we can get an exclusive lock on.
148
+ # If no jobs are left we return nil
149
+ def reserve_and_run_one_job
150
+
151
+ # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
152
+ # this leads to a more even distribution of jobs across the worker processes
153
+ job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
154
+ if job.lock_exclusively!(self.class.max_run_time, name)
155
+ say "* [Worker(#{name})] acquired lock on #{job.name}"
156
+ true
157
+ else
158
+ say "* [Worker(#{name})] failed to acquire exclusive lock for #{job.name}", Logger::WARN
159
+ false
160
+ end
161
+ end
162
+
163
+ run(job) if job
164
+ end
55
165
  end
56
166
  end
@@ -1,8 +1,8 @@
1
- autoload :ActiveRecord, 'activerecord'
1
+ 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/job'
5
+ require File.dirname(__FILE__) + '/delayed/backend/base'
6
6
  require File.dirname(__FILE__) + '/delayed/worker'
7
7
 
8
8
  Object.send(:include, Delayed::MessageSending)
@@ -0,0 +1,12 @@
1
+ require 'delayed_job'
2
+
3
+ config.after_initialize do
4
+ Delayed::Worker.backend ||= if defined?(ActiveRecord)
5
+ :active_record
6
+ elsif defined?(MongoMapper)
7
+ :mongo_mapper
8
+ else
9
+ Delayed::Worker.logger.warn "Could not decide on a backend, defaulting to active_record"
10
+ :active_record
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'delayed/backend/active_record'
3
+
4
+ describe Delayed::Job do
5
+ before(:all) do
6
+ @backend = Delayed::Job
7
+ end
8
+
9
+ before(:each) do
10
+ Delayed::Worker.max_priority = nil
11
+ Delayed::Worker.min_priority = nil
12
+ Delayed::Job.delete_all
13
+ SimpleJob.runs = 0
14
+ end
15
+
16
+ after do
17
+ Time.zone = nil
18
+ end
19
+
20
+ it_should_behave_like 'a backend'
21
+
22
+ context "db_time_now" do
23
+ it "should return time in current time zone if set" do
24
+ Time.zone = 'Eastern Time (US & Canada)'
25
+ %w(EST EDT).should include(Delayed::Job.db_time_now.zone)
26
+ end
27
+
28
+ it "should return UTC time if that is the AR default" do
29
+ Time.zone = nil
30
+ ActiveRecord::Base.default_timezone = :utc
31
+ Delayed::Job.db_time_now.zone.should == 'UTC'
32
+ end
33
+
34
+ it "should return local time if that is the AR default" do
35
+ Time.zone = 'Central Time (US & Canada)'
36
+ ActiveRecord::Base.default_timezone = :local
37
+ %w(CST CDT).should include(Delayed::Job.db_time_now.zone)
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ require 'delayed/backend/mongo_mapper'
4
+
5
+ describe Delayed::Backend::MongoMapper::Job do
6
+ before(:all) do
7
+ @backend = Delayed::Backend::MongoMapper::Job
8
+ end
9
+
10
+ before(:each) do
11
+ MongoMapper.database.collections.each(&:remove)
12
+ end
13
+
14
+ it_should_behave_like 'a backend'
15
+
16
+ describe "indexes" do
17
+ it "should have combo index on priority and run_at" do
18
+ @backend.collection.index_information.detect { |index| index[0] == 'priority_1_run_at_1' }.should_not be_nil
19
+ end
20
+
21
+ it "should have index on locked_by" do
22
+ @backend.collection.index_information.detect { |index| index[0] == 'locked_by_1' }.should_not be_nil
23
+ end
24
+ end
25
+
26
+ describe "delayed method" do
27
+ class MongoStoryReader
28
+ def read(story)
29
+ "Epilog: #{story.tell}"
30
+ end
31
+ end
32
+
33
+ class MongoStory
34
+ include ::MongoMapper::Document
35
+ key :text, String
36
+
37
+ def tell
38
+ text
39
+ end
40
+ end
41
+
42
+ it "should ignore not found errors because they are permanent" do
43
+ story = MongoStory.create :text => 'Once upon a time...'
44
+ job = story.send_later(:tell)
45
+ story.destroy
46
+ lambda { job.invoke_job }.should_not raise_error
47
+ end
48
+
49
+ it "should store the object as string" do
50
+ story = MongoStory.create :text => 'Once upon a time...'
51
+ job = story.send_later(:tell)
52
+
53
+ job.payload_object.class.should == Delayed::PerformableMethod
54
+ job.payload_object.object.should == "LOAD;MongoStory;#{story.id}"
55
+ job.payload_object.method.should == :tell
56
+ job.payload_object.args.should == []
57
+ job.payload_object.perform.should == 'Once upon a time...'
58
+ end
59
+
60
+ it "should store arguments as string" do
61
+ story = MongoStory.create :text => 'Once upon a time...'
62
+ job = MongoStoryReader.new.send_later(:read, story)
63
+ job.payload_object.class.should == Delayed::PerformableMethod
64
+ job.payload_object.method.should == :read
65
+ job.payload_object.args.should == ["LOAD;MongoStory;#{story.id}"]
66
+ job.payload_object.perform.should == 'Epilog: Once upon a time...'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,244 @@
1
+ shared_examples_for 'a backend' do
2
+ def create_job(opts = {})
3
+ @backend.create(opts.merge(:payload_object => SimpleJob.new))
4
+ end
5
+
6
+ before do
7
+ SimpleJob.runs = 0
8
+ end
9
+
10
+ it "should set run_at automatically if not set" do
11
+ @backend.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
12
+ end
13
+
14
+ it "should not set run_at automatically if already set" do
15
+ later = @backend.db_time_now + 5.minutes
16
+ @backend.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
17
+ end
18
+
19
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
20
+ lambda { @backend.enqueue(Object.new) }.should raise_error(ArgumentError)
21
+ end
22
+
23
+ it "should increase count after enqueuing items" do
24
+ @backend.enqueue SimpleJob.new
25
+ @backend.count.should == 1
26
+ end
27
+
28
+ it "should be able to set priority when enqueuing items" do
29
+ @job = @backend.enqueue SimpleJob.new, 5
30
+ @job.priority.should == 5
31
+ end
32
+
33
+ it "should be able to set run_at when enqueuing items" do
34
+ later = @backend.db_time_now + 5.minutes
35
+ @job = @backend.enqueue SimpleJob.new, 5, later
36
+ @job.run_at.should be_close(later, 1)
37
+ end
38
+
39
+ it "should work with jobs in modules" do
40
+ M::ModuleJob.runs = 0
41
+ job = @backend.enqueue M::ModuleJob.new
42
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
43
+ end
44
+
45
+ it "should raise an DeserializationError when the job class is totally unknown" do
46
+ job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
47
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
48
+ end
49
+
50
+ it "should try to load the class when it is unknown at the time of the deserialization" do
51
+ job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
52
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
53
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
54
+ end
55
+
56
+ it "should try include the namespace when loading unknown objects" do
57
+ job = @backend.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
58
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
59
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
60
+ end
61
+
62
+ it "should also try to load structs when they are unknown (raises TypeError)" do
63
+ job = @backend.new :handler => "--- !ruby/struct:JobThatDoesNotExist {}"
64
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
65
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
66
+ end
67
+
68
+ it "should try include the namespace when loading unknown structs" do
69
+ job = @backend.new :handler => "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
70
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
71
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
72
+ end
73
+
74
+ describe "find_available" do
75
+ it "should not find failed jobs" do
76
+ @job = create_job :attempts => 50, :failed_at => @backend.db_time_now
77
+ @backend.find_available('worker', 5, 1.second).should_not include(@job)
78
+ end
79
+
80
+ it "should not find jobs scheduled for the future" do
81
+ @job = create_job :run_at => (@backend.db_time_now + 1.minute)
82
+ @backend.find_available('worker', 5, 4.hours).should_not include(@job)
83
+ end
84
+
85
+ it "should not find jobs locked by another worker" do
86
+ @job = create_job(:locked_by => 'other_worker', :locked_at => @backend.db_time_now - 1.minute)
87
+ @backend.find_available('worker', 5, 4.hours).should_not include(@job)
88
+ end
89
+
90
+ it "should find open jobs" do
91
+ @job = create_job
92
+ @backend.find_available('worker', 5, 4.hours).should include(@job)
93
+ end
94
+
95
+ it "should find expired jobs" do
96
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now - 2.minutes)
97
+ @backend.find_available('worker', 5, 1.minute).should include(@job)
98
+ end
99
+
100
+ it "should find own jobs" do
101
+ @job = create_job(:locked_by => 'worker', :locked_at => (@backend.db_time_now - 1.minutes))
102
+ @backend.find_available('worker', 5, 4.hours).should include(@job)
103
+ end
104
+
105
+ it "should find only the right amount of jobs" do
106
+ 10.times { create_job }
107
+ @backend.find_available('worker', 7, 4.hours).should have(7).jobs
108
+ end
109
+ end
110
+
111
+ context "when another worker is already performing an task, it" do
112
+
113
+ before :each do
114
+ @job = @backend.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => @backend.db_time_now - 5.minutes
115
+ end
116
+
117
+ it "should not allow a second worker to get exclusive access" do
118
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
119
+ end
120
+
121
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
122
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
123
+ end
124
+
125
+ it "should be able to get access to the task if it was started more then max_age ago" do
126
+ @job.locked_at = 5.hours.ago
127
+ @job.save
128
+
129
+ @job.lock_exclusively! 4.hours, 'worker2'
130
+ @job.reload
131
+ @job.locked_by.should == 'worker2'
132
+ @job.locked_at.should > 1.minute.ago
133
+ end
134
+
135
+ it "should not be found by another worker" do
136
+ @backend.find_available('worker2', 1, 6.minutes).length.should == 0
137
+ end
138
+
139
+ it "should be found by another worker if the time has expired" do
140
+ @backend.find_available('worker2', 1, 4.minutes).length.should == 1
141
+ end
142
+
143
+ it "should be able to get exclusive access again when the worker name is the same" do
144
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
145
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
146
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
147
+ end
148
+ end
149
+
150
+ context "when another worker has worked on a task since the job was found to be available, it" do
151
+
152
+ before :each do
153
+ @job = @backend.create :payload_object => SimpleJob.new
154
+ @job_copy_for_worker_2 = @backend.find(@job.id)
155
+ end
156
+
157
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
158
+ @job.destroy
159
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
160
+ end
161
+
162
+ 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
163
+ @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
164
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
165
+ end
166
+ end
167
+
168
+ context "#name" do
169
+ it "should be the class name of the job that was enqueued" do
170
+ @backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
171
+ end
172
+
173
+ it "should be the method that will be called if its a performable method object" do
174
+ @job = Story.send_later(:create)
175
+ @job.name.should == "Story.create"
176
+ end
177
+
178
+ it "should be the instance method that will be called if its a performable method object" do
179
+ @job = Story.create(:text => "...").send_later(:save)
180
+ @job.name.should == 'Story#save'
181
+ end
182
+ end
183
+
184
+ context "worker prioritization" do
185
+ before(:each) do
186
+ Delayed::Worker.max_priority = nil
187
+ Delayed::Worker.min_priority = nil
188
+ end
189
+
190
+ it "should fetch jobs ordered by priority" do
191
+ 10.times { @backend.enqueue SimpleJob.new, rand(10) }
192
+ jobs = @backend.find_available('worker', 10)
193
+ jobs.size.should == 10
194
+ jobs.each_cons(2) do |a, b|
195
+ a.priority.should <= b.priority
196
+ end
197
+ end
198
+
199
+ it "should only find jobs greater than or equal to min priority" do
200
+ min = 5
201
+ Delayed::Worker.min_priority = min
202
+ 10.times {|i| @backend.enqueue SimpleJob.new, i }
203
+ jobs = @backend.find_available('worker', 10)
204
+ jobs.each {|job| job.priority.should >= min}
205
+ end
206
+
207
+ it "should only find jobs less than or equal to max priority" do
208
+ max = 5
209
+ Delayed::Worker.max_priority = max
210
+ 10.times {|i| @backend.enqueue SimpleJob.new, i }
211
+ jobs = @backend.find_available('worker', 10)
212
+ jobs.each {|job| job.priority.should <= max}
213
+ end
214
+ end
215
+
216
+ context "clear_locks!" do
217
+ before do
218
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
219
+ end
220
+
221
+ it "should clear locks for the given worker" do
222
+ @backend.clear_locks!('worker')
223
+ @backend.find_available('worker2', 5, 1.minute).should include(@job)
224
+ end
225
+
226
+ it "should not clear locks for other workers" do
227
+ @backend.clear_locks!('worker1')
228
+ @backend.find_available('worker1', 5, 1.minute).should_not include(@job)
229
+ end
230
+ end
231
+
232
+ context "unlock" do
233
+ before do
234
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
235
+ end
236
+
237
+ it "should clear locks" do
238
+ @job.unlock
239
+ @job.locked_by.should be_nil
240
+ @job.locked_at.should be_nil
241
+ end
242
+ end
243
+
244
+ end