public this repo is viewable by everyone
Description: Database based asynchronously priority queue system -- Extracted from Shopify
Homepage: http://www.shopify.com
Clone URL: git://github.com/tobi/delayed_job.git
Removed the global lock. Jobs can now processed in parallel by running 
multiple job runners on the same machine or across your server farm.
This requires two new columns in the job table: locked_by and locked_until
Tobias Lütke (author)
about 1 month ago
commit  8ec934eef96cd5622713eb7c621441cbafabe55d
tree    072d1ed344a90b07a4d0cd8f18db62d218e34b8a
parent  2309a943d87db9707a7d93999cfe6c264197b8bf
...
12
13
14
 
 
 
 
 
 
15
16
17
...
22
23
24
25
 
 
 
26
27
28
...
58
59
60
61
62
 
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
 
 
 
 
 
 
83
84
 
 
 
85
86
 
 
 
87
88
89
90
91
92
93
94
 
 
 
 
 
 
 
 
95
96
97
 
98
99
100
101
102
103
 
 
 
 
 
...
12
13
14
15
16
17
18
19
20
21
22
23
...
28
29
30
 
31
32
33
34
35
36
...
66
67
68
 
 
69
70
71
72
 
 
 
 
73
 
 
 
 
 
 
 
 
 
 
 
 
74
75
76
77
78
79
80
 
81
82
83
84
 
85
86
87
88
 
 
 
 
 
 
 
89
90
91
92
93
94
95
96
97
 
 
98
99
 
 
 
 
 
100
101
102
103
104
0
@@ -12,6 +12,12 @@ It is a direct extraction from Shopify where the job table is responsible for a
0
 * updating solr, our search server, after product changes
0
 * batch imports
0
 * spam checks
0
+
0
+== Changes ==
0
+
0
+1.5 Job runners can now be run in parallel. Two new database columns are needed: locked_until and locked_by. This allows us
0
+ to use pessimistic locking, which enables us to run as many worker processes as we need to speed up queue processing.
0
+1.0 Initial release
0
             
0
 == Setup ==
0
             
0
@@ -22,7 +28,9 @@ The library evolves around a delayed_jobs table which looks as follows:
0
     table.integer :attempts, :default => 0
0
     table.text :handler
0
     table.string :last_error
0
- table.datetime :run_at
0
+ table.datetime :run_at
0
+ table.datetime :locked_until
0
+ table.string :locked_by
0
     table.timestamps
0
   end
0
   
0
@@ -58,46 +66,39 @@ At Shopify we run the the tasks from a simple script/job_runner which is being i
0
   #!/usr/bin/env ruby
0
   require File.dirname(__FILE__) + '/../config/environment'
0
 
0
- SLEEP = 15
0
- RESTART_AFTER = 1000
0
+ SLEEP = 5
0
 
0
   trap('TERM') { puts 'Exiting...'; $exit = true }
0
   trap('INT') { puts 'Exiting...'; $exit = true }
0
-
0
- # this script dies after several runs to prevent memory leaks.
0
- # runnit will immediately start it again.
0
- count, runs_left = 0, RESTART_AFTER
0
 
0
- loop do
0
-
0
- count = 0
0
-
0
- # this requires the locking plugin, also from jadedPixel
0
- ActiveRecord::base.aquire_lock("jobs table worker", 10) do
0
- puts 'got lock'
0
-
0
- realtime = Benchmark.realtime do
0
- count = Delayed::Job.work_off
0
- end
0
- end
0
+ puts "*** Staring job worker #{Delayed::Job.worker_name}"
0
+
0
+ begin
0
+
0
+ loop do
0
+ result = nil
0
   
0
- runs_left -= 1
0
+ realtime = Benchmark.realtime do
0
+ result = Delayed::Job.work_off
0
+ end
0
   
0
- break if $exit
0
+ count = result.sum
0
+
0
+ break if $exit
0
   
0
- if count.zero?
0
- sleep(SLEEP)
0
- else
0
- status = "#{count} jobs completed at %.2f j/s ..." % [count / realtime]
0
- RAILS_DEFAULT_LOGGER.info status
0
- puts status
0
- end
0
+ if count.zero?
0
+ sleep(SLEEP)
0
+ puts 'Waiting for more jobs...'
0
+ else
0
+ status = "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
0
+ RAILS_DEFAULT_LOGGER.info status
0
+ puts status
0
+ end
0
     
0
- if $exit or runs_left <= 0
0
- break
0
+ break if $exit
0
     end
0
- end
0
-
0
-== Todo ==
0
-
0
-Work out a locking mechanism which would allow several job runners to run at the same time, spreading the load between them.
0
+
0
+ ensure
0
+ Delayed::Job.clear_locks!
0
+ end
0
+
...
4
5
6
7
8
9
10
11
12
13
 
 
 
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 
 
 
 
 
 
 
 
 
58
59
60
61
 
 
 
62
63
64
65
 
 
66
67
68
69
70
71
 
 
 
 
 
 
 
72
73
74
75
76
77
78
79
 
 
 
 
 
 
80
81
82
83
84
 
 
85
86
 
 
 
87
88
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
91
92
 
 
 
93
94
95
96
97
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
100
 
101
102
103
 
104
105
106
107
 
108
109
110
 
111
112
113
114
 
115
116
117
118
119
120
 
121
122
 
123
124
125
126
 
127
128
129
130
131
 
132
133
 
134
135
 
136
137
138
139
 
 
140
141
142
 
 
 
 
143
 
 
 
 
 
 
144
145
146
...
4
5
6
 
 
7
 
 
 
 
8
9
10
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
 
 
 
 
13
14
15
16
17
18
19
20
21
22
 
 
 
23
24
25
26
27
 
 
28
29
30
31
 
 
 
 
32
33
34
35
36
37
38
39
 
 
 
 
 
 
 
40
41
42
43
44
45
46
 
47
 
 
48
49
50
 
51
52
53
54
 
 
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
 
 
113
114
115
116
117
 
 
 
 
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
 
140
141
142
 
143
144
145
146
 
147
148
149
 
150
151
152
153
 
154
155
156
157
158
159
 
160
161
 
162
163
164
165
 
166
167
168
169
170
 
171
172
 
173
174
 
175
176
177
 
 
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
0
@@ -4,142 +4,192 @@ module Delayed
0
   end
0
 
0
   class Job < ActiveRecord::Base
0
- ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
0
-
0
     set_table_name :delayed_jobs
0
-
0
- class Runner
0
- attr_accessor :logger, :jobs
0
- attr_accessor :runs, :success, :failure
0
+
0
+ cattr_accessor :worker_name
0
+ self.worker_name = "pid:#{Process.pid}"
0
     
0
- def initialize(jobs, logger = nil)
0
- @jobs = jobs
0
- @logger = logger
0
- self.runs = self.success = self.failure = 0
0
- end
0
-
0
- def run
0
-
0
- ActiveRecord::Base.cache do
0
- ActiveRecord::Base.transaction do
0
- @jobs.each do |job|
0
- self.runs += 1
0
- begin
0
- time = Benchmark.measure do
0
- job.perform
0
- ActiveRecord::Base.uncached { job.destroy }
0
- self.success += 1
0
- end
0
- logger.debug "Executed job in #{time.real}"
0
- rescue DeserializationError, StandardError, RuntimeError => e
0
- if logger
0
- logger.error "Job #{job.id}: #{e.class} #{e.message}"
0
- logger.error e.backtrace.join("\n")
0
- end
0
- ActiveRecord::Base.uncached { job.reshedule e.message }
0
- self.failure += 1
0
- end
0
- end
0
- end
0
- end
0
-
0
- self
0
- end
0
- end
0
-
0
- def self.enqueue(object, priority = 0)
0
- raise ArgumentError, 'Cannot enqueue items which do not respond to perform' unless object.respond_to?(:perform)
0
     
0
- Job.create(:handler => object, :priority => priority)
0
- end
0
-
0
- def handler=(object)
0
- self['handler'] = object.to_yaml
0
+ NextTaskSQL = '`run_at` <= ? AND (`locked_until` IS NULL OR `locked_until` < ?) OR (`locked_by`=?)'
0
+ NextTaskOrder = 'priority DESC, run_at ASC'
0
+ ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
0
+
0
+ class LockError < StandardError
0
+ end
0
+
0
+ def self.clear_locks!
0
+ connection.execute "UPDATE #{table_name} SET `locked_by`=NULL, `locked_until`=NULL WHERE `locked_by`=#{quote_value(worker_name)}"
0
     end
0
-
0
- def handler
0
- @handler ||= deserialize(self['handler'])
0
+
0
+ def payload_object
0
+ @payload_object ||= deserialize(self['handler'])
0
     end
0
   
0
- def perform
0
- handler.perform
0
+ def payload_object=(object)
0
+ self['handler'] = object.to_yaml
0
     end
0
   
0
- def reshedule(message)
0
- self.attempts += 1
0
- self.run_at = self.class.time_now + (attempts ** 4).seconds
0
- self.last_error = message
0
+ def reshedule(message, time = nil)
0
+ time ||= Job.db_time_now + (attempts ** 4).seconds + 1
0
+
0
+ self.attempts += 1
0
+ self.run_at = time
0
+ self.last_error = message
0
+ self.unlock
0
       save!
0
- end
0
-
0
- def self.peek(limit = 1)
0
- if limit == 1
0
- find(:first, :order => "priority DESC, run_at ASC", :conditions => ['run_at <= ?', time_now])
0
- else
0
- find(:all, :order => "priority DESC, run_at ASC", :limit => limit, :conditions => ['run_at <= ?', time_now])
0
+ end
0
+
0
+
0
+ def self.enqueue(object, priority = 0)
0
+ unless object.respond_to?(:perform)
0
+ raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
0
       end
0
- end
0
     
0
- def self.work_off(limit = 100)
0
- jobs = Job.find(:all, :conditions => ['run_at <= ?', time_now], :order => "priority DESC, run_at ASC", :limit => limit)
0
+ Job.create(:payload_object => object, :priority => priority)
0
+ end
0
     
0
- Job::Runner.new(jobs, logger).run
0
+ def self.find_available(limit = 5)
0
+ time_now = db_time_now
0
+ find(:all, :conditions => [NextTaskSQL, time_now, time_now, worker_name], :order => NextTaskOrder, :limit => 5)
0
     end
0
-
0
- protected
0
+
0
+ # Get the payload of the next job we can get an exclusive lock on.
0
+ # If no jobs are left we return nil
0
+ def self.reserve(timeout = 5 * 60)
0
+
0
+ # We get up to 5 jobs from the db. In face we cannot get exclusive access to a job we try the next.
0
+ # this leads to a more even distribution of jobs across the worker processes
0
+ find_available(5).each do |job|
0
+ begin
0
+ job.lock_exclusively!(self.db_time_now + timeout, worker_name)
0
+ yield job.payload_object
0
+ job.destroy
0
+ return job
0
+ rescue LockError
0
+ # We did not get the lock, some other worker process must have
0
+ puts "failed to aquire exclusive lock for #{job.id}"
0
+ rescue StandardError => e
0
+ job.reshedule e.message
0
+ return job
0
+ end
0
+ end
0
+
0
+ nil
0
+ end
0
+
0
+ # This method is used internally by reserve method to ensure exclusive access
0
+ # to the given job. It will rise a LockError if it cannot get this lock.
0
+ def lock_exclusively!(lock_until, worker = worker_name)
0
+
0
+ affected_rows = if locked_by != worker
0
+
0
+ # We don't own this job so we will update the locked_by name and the locked_until
0
+ connection.update(<<-end_sql, "#{self.class.name} Update to aquire exclusive lock")
0
+ UPDATE #{self.class.table_name}
0
+ SET `locked_until`=#{quote_value(lock_until)}, `locked_by`=#{quote_value(worker)}
0
+ WHERE #{self.class.primary_key} = #{quote_value(id)} AND (`locked_until`<#{quote_value(self.class.db_time_now)} OR `locked_until` IS NULL)
0
+ end_sql
0
+
0
+ else
0
+
0
+ # We alrady own this job, this may happen if the job queue crashes.
0
+ # Simply update the lock timeout
0
+ connection.update(<<-end_sql, "#{self.class.name} Update exclusive lock")
0
+ UPDATE #{self.class.table_name}
0
+ SET `locked_until`=#{quote_value(lock_until)}
0
+ WHERE #{self.class.primary_key} = #{quote_value(id)} AND (`locked_by`=#{quote_value(worker)})
0
+ end_sql
0
+
0
+ end
0
+
0
+ unless affected_rows == 1
0
+ raise LockError, "Attempted to aquire exclusive lock failed"
0
+ end
0
+
0
+ self.locked_until = lock_until
0
+ self.locked_by = worker
0
+ end
0
     
0
- def self.time_now
0
- (ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.now
0
+ def unlock
0
+ self.locked_until = nil
0
+ self.locked_by = nil
0
     end
0
     
0
- def before_save
0
- self.run_at ||= self.class.time_now
0
- end
0
-
0
+ def self.work_off(num = 100)
0
+ success, failure = 0, 0
0
+
0
+ num.times do
0
+
0
+ job = self.reserve do |j|
0
+ begin
0
+ j.perform
0
+ success += 1
0
+ rescue
0
+ failure += 1
0
+ raise
0
+ end
0
+ end
0
+
0
+ break if job.nil?
0
+ end
0
+
0
+ return [success, failure]
0
+ end
0
+
0
     private
0
-
0
+
0
     def deserialize(source)
0
       attempt_to_load_file = true
0
-
0
+
0
       begin
0
         handler = YAML.load(source) rescue nil
0
         return handler if handler.respond_to?(:perform)
0
-
0
+
0
         if handler.nil?
0
           if source =~ ParseObjectFromYaml
0
-
0
+
0
             # Constantize the object so that ActiveSupport can attempt
0
             # its auto loading magic. Will raise LoadError if not successful.
0
             attempt_to_load($1)
0
-
0
+
0
             # If successful, retry the yaml.load
0
             handler = YAML.load(source)
0
             return handler if handler.respond_to?(:perform)
0
           end
0
         end
0
-
0
+
0
         if handler.is_a?(YAML::Object)
0
-
0
+
0
           # Constantize the object so that ActiveSupport can attempt
0
           # its auto loading magic. Will raise LoadError if not successful.
0
           attempt_to_load(handler.class)
0
-
0
+
0
           # If successful, retry the yaml.load
0
           handler = YAML.load(source)
0
           return handler if handler.respond_to?(:perform)
0
         end
0
-
0
+
0
         raise DeserializationError, 'Job failed to load: Unknown handler. Try to manually require the appropiate file.'
0
-
0
+
0
       rescue TypeError, LoadError, NameError => e
0
-
0
+
0
         raise DeserializationError, "Job failed to load: #{e.message}. Try to manually require the required file."
0
       end
0
- end
0
-
0
+ end
0
+
0
     def attempt_to_load(klass)
0
        klass.constantize
0
     end
0
+
0
+ def self.db_time_now
0
+ (ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.now
0
+ end
0
   
0
+ protected
0
+
0
+ def before_save
0
+ self.run_at ||= self.class.db_time_now
0
+ end
0
+
0
   end
0
 end
0
\ No newline at end of file
...
12
13
14
 
 
 
15
16
17
...
12
13
14
15
16
17
18
19
20
0
@@ -12,6 +12,9 @@ module Delayed
0
     
0
     def perform
0
       load(object).send(method, *args.map{|a| load(a)})
0
+ rescue ActiveRecord::RecordNotFound
0
+ # We cannot do anything about objects which were deleted in the meantime
0
+ true
0
     end
0
     
0
     private
...
12
13
14
15
16
17
18
 
 
 
 
19
 
 
20
21
22
...
12
13
14
 
 
 
 
15
16
17
18
19
20
21
22
23
24
0
@@ -12,11 +12,13 @@ def reset_db
0
   ActiveRecord::Schema.define do
0
 
0
     create_table :delayed_jobs, :force => true do |table|
0
- table.integer :priority, :default => 0
0
- table.integer :attempts, :default => 0
0
- table.text :handler
0
- table.string :last_error
0
+ table.integer :priority, :default => 0
0
+ table.integer :attempts, :default => 0
0
+ table.text :handler
0
+ table.string :last_error
0
       table.datetime :run_at
0
+ table.datetime :locked_until
0
+ table.string :locked_by
0
       table.timestamps
0
     end
0
 
...
48
49
50
51
52
53
54
55
 
56
57
58
59
60
61
62
63
64
 
 
 
 
 
 
65
66
67
...
71
72
73
74
75
76
77
78
 
 
 
 
 
79
80
81
82
...
48
49
50
 
51
52
 
 
53
54
55
56
 
 
 
 
 
 
57
58
59
60
61
62
63
64
65
...
69
70
71
 
 
 
 
 
72
73
74
75
76
77
78
79
80
0
@@ -48,20 +48,18 @@ describe 'random ruby objects' do
0
     RandomRubyObject.new.send_later(:say_hello)
0
                                
0
     Delayed::Job.count.should == 1
0
- Delayed::Job.peek.perform.should == 'hello'
0
   end
0
   
0
- it "should store the object as string if its an active record" do
0
-
0
+ it "should store the object as string if its an active record" do
0
     story = Story.create :text => 'Once upon...'
0
     story.send_later(:tell)
0
     
0
- job = Delayed::Job.peek
0
- job.handler.class.should == Delayed::PerformableMethod
0
- job.handler.object.should == 'AR:Story:1'
0
- job.handler.method.should == :tell
0
- job.handler.args.should == []
0
- job.perform.should == 'Once upon...'
0
+ job = Delayed::Job.find(:first)
0
+ job.payload_object.class.should == Delayed::PerformableMethod
0
+ job.payload_object.object.should == 'AR:Story:1'
0
+ job.payload_object.method.should == :tell
0
+ job.payload_object.args.should == []
0
+ job.payload_object.perform.should == 'Once upon...'
0
   end
0
   
0
   it "should store arguments as string if they an active record" do
0
@@ -71,11 +69,11 @@ describe 'random ruby objects' do
0
     reader = StoryReader.new
0
     reader.send_later(:read, story)
0
     
0
- job = Delayed::Job.peek
0
- job.handler.class.should == Delayed::PerformableMethod
0
- job.handler.method.should == :read
0
- job.handler.args.should == ['AR:Story:1']
0
- job.perform.should == 'Epilog: Once upon...'
0
+ job = Delayed::Job.find(:first)
0
+ job.payload_object.class.should == Delayed::PerformableMethod
0
+ job.payload_object.method.should == :read
0
+ job.payload_object.args.should == ['AR:Story:1']
0
+ job.payload_object.perform.should == 'Epilog: Once upon...'
0
   end
0
   
0
 end
0
\ No newline at end of file
...
1
2
3
4
5
6
...
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
52
53
54
...
56
57
58
59
60
61
 
62
63
64
...
71
72
73
74
 
75
76
77
...
80
81
82
83
 
84
85
86
87
88
89
90
 
91
92
93
...
97
98
99
100
 
101
102
103
104
105
106
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
110
111
...
1
2
 
3
4
5
...
29
30
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
33
34
35
 
36
37
38
39
...
41
42
43
 
 
 
44
45
46
47
...
54
55
56
 
57
58
59
60
...
63
64
65
 
66
67
68
69
70
71
72
 
73
74
75
76
...
80
81
82
 
83
84
85
86
87
88
89
 
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
0
@@ -1,6 +1,5 @@
0
 require File.dirname(__FILE__) + '/database'
0
 
0
-
0
 class SimpleJob
0
   cattr_accessor :runs; self.runs = 0
0
   def perform; @@runs += 1; end
0
@@ -30,25 +29,11 @@ describe Delayed::Job do
0
     Delayed::Job.count.should == 1
0
   end
0
   
0
- it "should return nil when peeking on empty table" do
0
- Delayed::Job.peek.should == nil
0
- end
0
-
0
- it "should return a job when peeking a table with jobs in it" do
0
- Delayed::Job.enqueue SimpleJob.new
0
- Delayed::Job.peek.class.should == Delayed::Job
0
- end
0
-
0
- it "should return an array of jobs when peek is called with a count larger than zero" do
0
- Delayed::Job.enqueue SimpleJob.new
0
- Delayed::Job.peek(2).class.should == Array
0
- end
0
-
0
   it "should call perform on jobs when running work_off" do
0
     SimpleJob.runs.should == 0
0
         
0
     Delayed::Job.enqueue SimpleJob.new
0
- Delayed::Job.work_off(1)
0
+ Delayed::Job.work_off
0
     
0
     SimpleJob.runs.should == 1
0
   end
0
@@ -56,9 +41,7 @@ describe Delayed::Job do
0
   it "should re-schedule by about 1 second at first and increment this more and more minutes when it fails to execute properly" do
0
     Delayed::Job.enqueue ErrorJob.new
0
     runner = Delayed::Job.work_off(1)
0
- runner.success.should == 0
0
- runner.failure.should == 1
0
-
0
+
0
     job = Delayed::Job.find(:first)
0
     job.last_error.should == 'did not work'
0
     job.attempts.should == 1
0
@@ -71,7 +54,7 @@ describe Delayed::Job do
0
     job = Delayed::Job.new
0
     job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
0
 
0
- lambda { job.perform }.should raise_error(Delayed::DeserializationError)
0
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
0
   end
0
 
0
   it "should try to load the class when it is unknown at the time of the deserialization" do
0
@@ -80,14 +63,14 @@ describe Delayed::Job do
0
 
0
     job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
0
      
0
- lambda { job.perform }.should raise_error(Delayed::DeserializationError)
0
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
0
   end
0
   
0
   it "should try include the namespace when loading unknown objects" do
0
     job = Delayed::Job.new
0
     job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
0
     job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
0
- lambda { job.perform }.should raise_error(Delayed::DeserializationError)
0
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
0
   end
0
   
0
   
0
@@ -97,15 +80,36 @@ describe Delayed::Job do
0
 
0
     job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
0
      
0
- lambda { job.perform }.should raise_error(Delayed::DeserializationError)
0
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
0
   end
0
   
0
   it "should try include the namespace when loading unknown structs" do
0
     job = Delayed::Job.new
0
     job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
0
     job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
0
- lambda { job.perform }.should raise_error(Delayed::DeserializationError)
0
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
0
   end
0
+
0
+
0
+ describe "when two workers are running" do
0
+
0
+ before :each do
0
+ Delayed::Job.worker_name = 'worker1'
0
+ Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_until => Time.now + 360
0
+ end
0
+
0
+ it "should give exclusive access only to a single worker" do
0
+ job = Delayed::Job.find_available.first
0
+ lambda { job.lock_exclusively! Time.now + 20, 'worker2' }.should raise_error(Delayed::Job::LockError)
0
+ end
0
+
0
+ it "should be able to get exclusive access again when the worker name is the same" do
0
+ job = Delayed::Job.find_available.first
0
+ job.lock_exclusively! Time.now + 20, 'worker1'
0
+ job.lock_exclusively! Time.now + 21, 'worker1'
0
+ job.lock_exclusively! Time.now + 22, 'worker1'
0
+ end
0
+ end
0
   
0
 end
0
 

Comments

    No one has commented yet.