delayed_job 2.1.0.pre2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,6 +1,6 @@
1
1
  h1. Delayed::Job
2
2
 
3
- Delated_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background.
3
+ Delated_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background.
4
4
 
5
5
  It is a direct extraction from Shopify where the job table is responsible for a multitude of core tasks. Amongst those tasks are:
6
6
 
@@ -9,31 +9,17 @@ It is a direct extraction from Shopify where the job table is responsible for a
9
9
  * http downloads
10
10
  * updating smart collections
11
11
  * updating solr, our search server, after product changes
12
- * batch imports
13
- * spam checks
12
+ * batch imports
13
+ * spam checks
14
14
 
15
15
  h2. Installation
16
16
 
17
- To install as a gem, add the following to @config/environment.rb@:
17
+ delayed_job 2.1 only supports Rails 3.0+. See the "2.0 branch":https://github.com/collectiveidea/delayed_job/tree/v2.0 for Rails 2.
18
18
 
19
- <pre>
20
- config.gem 'delayed_job'
21
- </pre>
22
-
23
- Rake tasks are not automatically loaded from gems, so you'll need to add the following to your Rakefile:
19
+ To install, add delayed_job to your @Gemfile@ and run `bundle install`:
24
20
 
25
21
  <pre>
26
- begin
27
- require 'delayed/tasks'
28
- rescue LoadError
29
- STDERR.puts "Run `rake gems:install` to install delayed_job"
30
- end
31
- </pre>
32
-
33
- To install as a plugin:
34
-
35
- <pre>
36
- script/plugin install git://github.com/collectiveidea/delayed_job.git
22
+ gem 'delayed_job'
37
23
  </pre>
38
24
 
39
25
  After delayed_job is installed, you will need to setup the backend.
@@ -42,12 +28,10 @@ h2. Backends
42
28
 
43
29
  delayed_job supports multiple backends for storing the job queue. "See the wiki for other backends":http://wiki.github.com/collectiveidea/delayed_job/backends besides Active Record.
44
30
 
45
- h3. Active Record
46
-
47
31
  The default is Active Record, which requires a jobs table.
48
32
 
49
33
  <pre>
50
- $ script/generate delayed_job
34
+ $ script/rails generate delayed_job
51
35
  $ rake db:migrate
52
36
  </pre>
53
37
 
@@ -60,7 +44,7 @@ Call @.delay.method(params)@ on any object and it will be processed in the backg
60
44
  Notifier.deliver_signup(@user)
61
45
 
62
46
  # with delayed_job
63
- Notifier.delay.deliver_signup @user
47
+ Notifier.delay.signup(@user)
64
48
  </pre>
65
49
 
66
50
  If a method should always be run in the background, you can call @#handle_asynchronously@ after the method declaration:
@@ -77,6 +61,39 @@ device = Device.new
77
61
  device.deliver
78
62
  </pre>
79
63
 
64
+ handle_asynchronously can take as options anything you can pass to delay. In addition the values can be Proc objects allowing call time evaluation of the value. For some examples:
65
+
66
+ <pre>
67
+ class LongTasks
68
+ def send_mailer
69
+ # Some other code
70
+ end
71
+ handle_asynchronously :send_mailer, :priority => 20
72
+
73
+ def in_the_future
74
+ # Some other code
75
+ end
76
+ # 5.minutes.from_now will be evaluated when in_the_future is called
77
+ handle_asynchronously :in_the_future, :run_at => Proc.new { 5.minutes.from_now }
78
+
79
+ def self.when_to_run
80
+ 2.hours.from_now
81
+ end
82
+
83
+ def call_a_class_method
84
+ # Some other code
85
+ end
86
+ handle_asynchronously :call_a_class_method, :run_at => Proc.new { when_to_run }
87
+
88
+ attr_reader :how_important
89
+
90
+ def call_an_instance_method
91
+ # Some other code
92
+ end
93
+ handle_asynchronously :call_an_instance_method, :priority => Proc.new {|i| i.how_important }
94
+ end
95
+ </pre>
96
+
80
97
  h2. Running Jobs
81
98
 
82
99
  @script/delayed_job@ can be used to manage a background process which will start working off jobs. Make sure you've run `script/generate delayed_job`.
@@ -92,39 +109,61 @@ $ RAILS_ENV=production script/delayed_job stop
92
109
 
93
110
  Workers can be running on any computer, as long as they have access to the database and their clock is in sync. Keep in mind that each worker will check the database at least every 5 seconds.
94
111
 
95
- You can also invoke @rake jobs:work@ which will start working off jobs. You can cancel the rake task with @CTRL-C@.
112
+ You can also invoke @rake jobs:work@ which will start working off jobs. You can cancel the rake task with @CTRL-C@.
96
113
 
97
114
  h2. Custom Jobs
98
115
 
99
- Jobs are simple ruby objects with a method called perform. Any object which responds to perform can be stuffed into the jobs table. Job objects are serialized to yaml so that they can later be resurrected by the job runner.
116
+ Jobs are simple ruby objects with a method called perform. Any object which responds to perform can be stuffed into the jobs table. Job objects are serialized to yaml so that they can later be resurrected by the job runner.
100
117
 
101
118
  <pre>
102
119
  class NewsletterJob < Struct.new(:text, :emails)
103
120
  def perform
104
121
  emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
105
- end
106
- end
107
-
122
+ end
123
+ end
124
+
108
125
  Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))
109
126
  </pre>
110
127
 
111
- You can also add an optional on_permanent_failure method which will run if the job has failed too many times to be retried:
128
+ h2. Hooks
129
+
130
+ You can define hooks on your job that will be called at different stages in the process:
112
131
 
113
132
  <pre>
114
133
  class ParanoidNewsletterJob < NewsletterJob
134
+ def enqueue(job)
135
+ record_stat 'newsletter_job/enqueue'
136
+ end
137
+
115
138
  def perform
116
139
  emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
117
- end
140
+ end
141
+
142
+ def before(job)
143
+ record_stat 'newsletter_job/start'
144
+ end
118
145
 
119
- def on_permanent_failure
146
+ def after(job)
147
+ record_stat 'newsletter_job/after'
148
+ end
149
+
150
+ def success(job)
151
+ record_stat 'newsletter_job/success'
152
+ end
153
+
154
+ def error(job, exception)
155
+ notify_hoptoad(exception)
156
+ end
157
+
158
+ def failure
120
159
  page_sysadmin_in_the_middle_of_the_night
121
160
  end
122
- end
161
+ end
123
162
  </pre>
124
163
 
125
164
  h2. Gory Details
126
165
 
127
- The library evolves around a delayed_jobs table which looks as follows:
166
+ The library evolves around a delayed_jobs table which looks as follows:
128
167
 
129
168
  <pre>
130
169
  create_table :delayed_jobs, :force => true do |table|
@@ -151,6 +190,8 @@ make sure your job doesn't exceed this time. You should set this to the longest
151
190
  By default, it will delete failed jobs (and it always deletes successful jobs). If you want to keep failed jobs, set
152
191
  Delayed::Worker.destroy_failed_jobs = false. The failed jobs will be marked with non-null failed_at.
153
192
 
193
+ By default all jobs are scheduled with priority = 0, which is top priority. You can change this by setting Delayed::Worker.default_priority to something else. Lower numbers have higher priority.
194
+
154
195
  Here is an example of changing job parameters in Rails:
155
196
 
156
197
  <pre>
@@ -173,19 +214,13 @@ h2. How to contribute
173
214
 
174
215
  If you find what looks like a bug:
175
216
 
176
- # Check the GitHub issue tracker to see if anyone else has had the same issue.
177
- http://github.com/collectiveidea/delayed_job/issues/
217
+ # Search the "mailing list":http://groups.google.com/group/delayed_job to see if anyone else had the same issue.
218
+ # Check the "GitHub issue tracker":http://github.com/collectiveidea/delayed_job/issues/ to see if anyone else has reported issue.
178
219
  # If you don't see anything, create an issue with information on how to reproduce it.
179
220
 
180
221
  If you want to contribute an enhancement or a fix:
181
222
 
182
223
  # Fork the project on github.
183
- http://github.com/collectiveidea/delayed_job/
184
224
  # Make your changes with tests.
185
- # Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
225
+ # Commit the changes without making changes to the Rakefile or any other files that aren't related to your enhancement or fix
186
226
  # Send a pull request.
187
-
188
- h3. Changelog
189
-
190
- See http://wiki.github.com/collectiveidea/delayed_job/changelog for a list of changes.
191
-
@@ -1,19 +1,5 @@
1
1
  require 'active_record'
2
2
 
3
- class ActiveRecord::Base
4
- yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
5
-
6
- def self.yaml_new(klass, tag, val)
7
- klass.find(val['attributes']['id'])
8
- rescue ActiveRecord::RecordNotFound
9
- nil
10
- end
11
-
12
- def to_yaml_properties
13
- ['@attributes']
14
- end
15
- end
16
-
17
3
  module Delayed
18
4
  module Backend
19
5
  module ActiveRecord
@@ -22,23 +8,20 @@ module Delayed
22
8
  class Job < ::ActiveRecord::Base
23
9
  include Delayed::Backend::Base
24
10
  set_table_name :delayed_jobs
25
-
11
+
26
12
  before_save :set_default_run_at
27
13
 
28
- if ::ActiveRecord::VERSION::MAJOR >= 3
29
- scope :ready_to_run, lambda {|worker_name, max_run_time|
30
- where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
31
- }
32
- scope :by_priority, order('priority ASC, run_at ASC')
33
- else
34
- named_scope :ready_to_run, lambda {|worker_name, max_run_time|
35
- {:conditions => ['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name]}
36
- }
37
- named_scope :by_priority, :order => 'priority ASC, run_at ASC'
14
+ scope :ready_to_run, lambda {|worker_name, max_run_time|
15
+ where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
16
+ }
17
+ scope :by_priority, order('priority ASC, run_at ASC')
18
+
19
+ def self.before_fork
20
+ ::ActiveRecord::Base.clear_all_connections!
38
21
  end
39
22
 
40
23
  def self.after_fork
41
- ::ActiveRecord::Base.connection.reconnect!
24
+ ::ActiveRecord::Base.establish_connection
42
25
  end
43
26
 
44
27
  # When a worker is exiting, make sure we don't have any locked jobs.
@@ -51,7 +34,7 @@ module Delayed
51
34
  scope = self.ready_to_run(worker_name, max_run_time)
52
35
  scope = scope.scoped(:conditions => ['priority >= ?', Worker.min_priority]) if Worker.min_priority
53
36
  scope = scope.scoped(:conditions => ['priority <= ?', Worker.max_priority]) if Worker.max_priority
54
-
37
+
55
38
  ::ActiveRecord::Base.silence do
56
39
  scope.by_priority.all(:limit => limit)
57
40
  end
@@ -70,8 +53,10 @@ module Delayed
70
53
  self.class.update_all(["locked_at = ?", now], ["id = ? and locked_by = ?", id, worker])
71
54
  end
72
55
  if affected_rows == 1
73
- self.locked_at = now
74
- self.locked_by = worker
56
+ self.locked_at = now
57
+ self.locked_by = worker
58
+ self.locked_at_will_change!
59
+ self.locked_by_will_change!
75
60
  return true
76
61
  else
77
62
  return false
@@ -1,91 +1,120 @@
1
1
  module Delayed
2
2
  module Backend
3
- class DeserializationError < StandardError
4
- end
5
-
6
3
  module Base
7
4
  def self.included(base)
8
5
  base.extend ClassMethods
9
6
  end
10
-
7
+
11
8
  module ClassMethods
12
9
  # Add a job to the queue
13
10
  def enqueue(*args)
14
- object = args.shift
15
- unless object.respond_to?(:perform)
11
+ options = {
12
+ :priority => Delayed::Worker.default_priority
13
+ }
14
+
15
+ if args.size == 1 && args.first.is_a?(Hash)
16
+ options.merge!(args.first)
17
+ else
18
+ options[:payload_object] = args.shift
19
+ options[:priority] = args.first || options[:priority]
20
+ options[:run_at] = args[1]
21
+ end
22
+
23
+ unless options[:payload_object].respond_to?(:perform)
16
24
  raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
17
25
  end
18
-
19
- priority = args.first || Delayed::Worker.default_priority
20
- run_at = args[1]
21
- self.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
26
+
27
+ self.create(options).tap do |job|
28
+ job.hook(:enqueue)
29
+ end
30
+ end
31
+
32
+ def reserve(worker, max_run_time = Worker.max_run_time)
33
+ # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
34
+ # this leads to a more even distribution of jobs across the worker processes
35
+ find_available(worker.name, 5, max_run_time).detect do |job|
36
+ job.lock_exclusively!(max_run_time, worker.name)
37
+ end
22
38
  end
23
-
39
+
24
40
  # Hook method that is called before a new worker is forked
25
41
  def before_fork
26
42
  end
27
-
43
+
28
44
  # Hook method that is called after a new worker is forked
29
45
  def after_fork
30
46
  end
31
-
47
+
32
48
  def work_off(num = 100)
33
49
  warn "[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead."
34
50
  Delayed::Worker.new.work_off(num)
35
51
  end
36
52
  end
37
-
38
- ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
39
53
 
40
54
  def failed?
41
55
  failed_at
42
56
  end
43
57
  alias_method :failed, :failed?
44
58
 
59
+ ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
60
+
45
61
  def name
46
- @name ||= begin
47
- payload = payload_object
48
- payload.respond_to?(:display_name) ? payload.display_name : payload.class.name
49
- end
62
+ @name ||= payload_object.respond_to?(:display_name) ?
63
+ payload_object.display_name :
64
+ payload_object.class.name
65
+ rescue DeserializationError
66
+ ParseObjectFromYaml.match(handler)[1]
50
67
  end
51
68
 
52
69
  def payload_object=(object)
70
+ @payload_object = object
53
71
  self.handler = object.to_yaml
54
72
  end
55
-
73
+
56
74
  def payload_object
57
75
  @payload_object ||= YAML.load(self.handler)
58
- rescue TypeError, LoadError, NameError => e
59
- raise DeserializationError,
60
- "Job failed to load: #{e.message}. Try to manually require the required file. Handler: #{handler.inspect}"
76
+ rescue TypeError, LoadError, NameError, ArgumentError => e
77
+ raise DeserializationError,
78
+ "Job failed to load: #{e.message}. Handler: #{handler.inspect}"
61
79
  end
62
80
 
63
- # Moved into its own method so that new_relic can trace it.
64
81
  def invoke_job
65
- payload_object.before(self) if payload_object.respond_to?(:before)
66
- begin
67
- payload_object.perform
68
- payload_object.success(self) if payload_object.respond_to?(:success)
69
- rescue Exception => e
70
- payload_object.failure(self, e) if payload_object.respond_to?(:failure)
71
- raise e
72
- ensure
73
- payload_object.after(self) if payload_object.respond_to?(:after)
74
- end
82
+ hook :before
83
+ payload_object.perform
84
+ hook :success
85
+ rescue Exception => e
86
+ hook :error, e
87
+ raise e
88
+ ensure
89
+ hook :after
75
90
  end
76
-
91
+
77
92
  # Unlock this job (note: not saved to DB)
78
93
  def unlock
79
94
  self.locked_at = nil
80
95
  self.locked_by = nil
81
96
  end
82
-
97
+
98
+ def hook(name, *args)
99
+ if payload_object.respond_to?(name)
100
+ method = payload_object.method(name)
101
+ method.arity == 0 ? method.call : method.call(self, *args)
102
+ end
103
+ rescue DeserializationError
104
+ # do nothing
105
+ end
106
+
107
+ def reschedule_at
108
+ payload_object.respond_to?(:reschedule_at) ?
109
+ payload_object.reschedule_at(self.class.db_time_now, attempts) :
110
+ self.class.db_time_now + (attempts ** 4) + 5
111
+ end
112
+
83
113
  protected
84
114
 
85
115
  def set_default_run_at
86
116
  self.run_at ||= self.class.db_time_now
87
117
  end
88
-
89
118
  end
90
119
  end
91
120
  end
@@ -12,156 +12,168 @@ shared_examples_for 'a delayed_job backend' do
12
12
  SimpleJob.runs = 0
13
13
  described_class.delete_all
14
14
  end
15
-
15
+
16
16
  it "should set run_at automatically if not set" do
17
17
  described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
18
18
  end
19
19
 
20
20
  it "should not set run_at automatically if already set" do
21
21
  later = described_class.db_time_now + 5.minutes
22
- described_class.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
22
+ job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
23
+ job.run_at.should be_within(1).of(later)
23
24
  end
24
-
25
+
25
26
  describe "enqueue" do
26
- it "should raise ArgumentError when handler doesn't respond_to :perform" do
27
- lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
28
- end
27
+ context "with a hash" do
28
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
29
+ lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
30
+ end
29
31
 
30
- it "should increase count after enqueuing items" do
31
- described_class.enqueue SimpleJob.new
32
- described_class.count.should == 1
33
- end
32
+ it "should be able to set priority" do
33
+ job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
34
+ job.priority.should == 5
35
+ end
34
36
 
35
- it "should be able to set priority" do
36
- @job = described_class.enqueue SimpleJob.new, 5
37
- @job.priority.should == 5
38
- end
37
+ it "should use default priority" do
38
+ job = described_class.enqueue :payload_object => SimpleJob.new
39
+ job.priority.should == 99
40
+ end
39
41
 
40
- it "should use default priority when it is not set" do
41
- @job = described_class.enqueue SimpleJob.new
42
- @job.priority.should == 99
42
+ it "should be able to set run_at" do
43
+ later = described_class.db_time_now + 5.minutes
44
+ job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
45
+ job.run_at.should be_within(1).of(later)
46
+ end
43
47
  end
44
48
 
45
- it "should be able to set run_at" do
46
- later = described_class.db_time_now + 5.minutes
47
- @job = described_class.enqueue SimpleJob.new, 5, later
48
- @job.run_at.should be_close(later, 1)
49
- end
49
+ context "with multiple arguments" do
50
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
51
+ lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
52
+ end
50
53
 
51
- it "should work with jobs in modules" do
52
- M::ModuleJob.runs = 0
53
- job = described_class.enqueue M::ModuleJob.new
54
- lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
54
+ it "should increase count after enqueuing items" do
55
+ described_class.enqueue SimpleJob.new
56
+ described_class.count.should == 1
57
+ end
58
+
59
+ it "should be able to set priority" do
60
+ @job = described_class.enqueue SimpleJob.new, 5
61
+ @job.priority.should == 5
62
+ end
63
+
64
+ it "should use default priority when it is not set" do
65
+ @job = described_class.enqueue SimpleJob.new
66
+ @job.priority.should == 99
67
+ end
68
+
69
+ it "should be able to set run_at" do
70
+ later = described_class.db_time_now + 5.minutes
71
+ @job = described_class.enqueue SimpleJob.new, 5, later
72
+ @job.run_at.should be_within(1).of(later)
73
+ end
74
+
75
+ it "should work with jobs in modules" do
76
+ M::ModuleJob.runs = 0
77
+ job = described_class.enqueue M::ModuleJob.new
78
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
79
+ end
55
80
  end
56
81
  end
57
-
82
+
58
83
  describe "callbacks" do
59
84
  before(:each) do
60
- SuccessfulCallbackJob.messages = []
61
- FailureCallbackJob.messages = []
85
+ CallbackJob.messages = []
62
86
  end
63
-
87
+
88
+ %w(before success after).each do |callback|
89
+ it "should call #{callback} with job" do
90
+ job = described_class.enqueue(CallbackJob.new)
91
+ job.payload_object.should_receive(callback).with(job)
92
+ job.invoke_job
93
+ end
94
+ end
95
+
64
96
  it "should call before and after callbacks" do
65
- job = described_class.enqueue(SuccessfulCallbackJob.new)
97
+ job = described_class.enqueue(CallbackJob.new)
98
+ CallbackJob.messages.should == ["enqueue"]
66
99
  job.invoke_job
67
- SuccessfulCallbackJob.messages.should == ["before perform", "perform", "success!", "after perform"]
100
+ CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after"]
68
101
  end
69
102
 
70
103
  it "should call the after callback with an error" do
71
- job = described_class.enqueue(FailureCallbackJob.new)
72
- lambda {job.invoke_job}.should raise_error
73
- FailureCallbackJob.messages.should == ["before perform", "error: RuntimeError", "after perform"]
104
+ job = described_class.enqueue(CallbackJob.new)
105
+ job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
106
+
107
+ lambda { job.invoke_job }.should raise_error
108
+ CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
109
+ end
110
+
111
+ it "should call error when before raises an error" do
112
+ job = described_class.enqueue(CallbackJob.new)
113
+ job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
114
+ lambda { job.invoke_job }.should raise_error(RuntimeError)
115
+ CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
74
116
  end
75
-
76
117
  end
77
-
118
+
78
119
  describe "payload_object" do
79
120
  it "should raise a DeserializationError when the job class is totally unknown" do
80
121
  job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
81
- lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
122
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
82
123
  end
83
124
 
84
125
  it "should raise a DeserializationError when the job struct is totally unknown" do
85
126
  job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
86
- lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
127
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
87
128
  end
88
- end
89
-
90
- describe "find_available" do
91
- it "should not find failed jobs" do
92
- @job = create_job :attempts => 50, :failed_at => described_class.db_time_now
93
- described_class.find_available('worker', 5, 1.second).should_not include(@job)
94
- end
95
-
96
- it "should not find jobs scheduled for the future" do
97
- @job = create_job :run_at => (described_class.db_time_now + 1.minute)
98
- described_class.find_available('worker', 5, 4.hours).should_not include(@job)
99
- end
100
-
101
- it "should not find jobs locked by another worker" do
102
- @job = create_job(:locked_by => 'other_worker', :locked_at => described_class.db_time_now - 1.minute)
103
- described_class.find_available('worker', 5, 4.hours).should_not include(@job)
104
- end
105
-
106
- it "should find open jobs" do
107
- @job = create_job
108
- described_class.find_available('worker', 5, 4.hours).should include(@job)
109
- end
110
-
111
- it "should find expired jobs" do
112
- @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now - 2.minutes)
113
- described_class.find_available('worker', 5, 1.minute).should include(@job)
114
- end
115
-
116
- it "should find own jobs" do
117
- @job = create_job(:locked_by => 'worker', :locked_at => (described_class.db_time_now - 1.minutes))
118
- described_class.find_available('worker', 5, 4.hours).should include(@job)
119
- end
120
-
121
- it "should find only the right amount of jobs" do
122
- 10.times { create_job }
123
- described_class.find_available('worker', 7, 4.hours).should have(7).jobs
129
+
130
+ it "should raise a DeserializationError when the YAML.load raises argument error" do
131
+ job = described_class.find(create_job.id)
132
+ YAML.should_receive(:load).and_raise(ArgumentError)
133
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
124
134
  end
125
135
  end
126
-
127
- context "when another worker is already performing an task, it" do
128
- before :each do
129
- @job = described_class.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => described_class.db_time_now - 5.minutes
136
+
137
+ describe "reserve" do
138
+ before do
139
+ Delayed::Worker.max_run_time = 2.minutes
140
+ @worker = Delayed::Worker.new(:quiet => true)
130
141
  end
131
142
 
132
- it "should not allow a second worker to get exclusive access" do
133
- @job.lock_exclusively!(4.hours, 'worker2').should == false
143
+ it "should not reserve failed jobs" do
144
+ create_job :attempts => 50, :failed_at => described_class.db_time_now
145
+ described_class.reserve(@worker).should be_nil
134
146
  end
135
147
 
136
- it "should allow a second worker to get exclusive access if the timeout has passed" do
137
- @job.lock_exclusively!(1.minute, 'worker2').should == true
138
- end
139
-
140
- it "should be able to get access to the task if it was started more then max_age ago" do
141
- @job.locked_at = described_class.db_time_now - 5.hours
142
- @job.save
148
+ it "should not reserve jobs scheduled for the future" do
149
+ create_job :run_at => (described_class.db_time_now + 1.minute)
150
+ described_class.reserve(@worker).should be_nil
151
+ end
143
152
 
144
- @job.lock_exclusively! 4.hours, 'worker2'
145
- @job.reload
146
- @job.locked_by.should == 'worker2'
147
- @job.locked_at.should > (described_class.db_time_now - 1.minute)
153
+ it "should lock the job so other workers can't reserve it" do
154
+ job = create_job
155
+ described_class.reserve(@worker).should == job
156
+ new_worker = Delayed::Worker.new(:quiet => true)
157
+ new_worker.name = 'worker2'
158
+ described_class.reserve(new_worker).should be_nil
148
159
  end
149
160
 
150
- it "should not be found by another worker" do
151
- described_class.find_available('worker2', 1, 6.minutes).length.should == 0
161
+ it "should reserve open jobs" do
162
+ job = create_job
163
+ described_class.reserve(@worker).should == job
152
164
  end
153
165
 
154
- it "should be found by another worker if the time has expired" do
155
- described_class.find_available('worker2', 1, 4.minutes).length.should == 1
166
+ it "should reserve expired jobs" do
167
+ job = create_job(:locked_by => @worker.name, :locked_at => described_class.db_time_now - 3.minutes)
168
+ described_class.reserve(@worker).should == job
156
169
  end
157
170
 
158
- it "should be able to get exclusive access again when the worker name is the same" do
159
- @job.lock_exclusively!(5.minutes, 'worker1').should be_true
160
- @job.lock_exclusively!(5.minutes, 'worker1').should be_true
161
- @job.lock_exclusively!(5.minutes, 'worker1').should be_true
162
- end
171
+ it "should reserve own jobs" do
172
+ job = create_job(:locked_by => @worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
173
+ described_class.reserve(@worker).should == job
174
+ end
163
175
  end
164
-
176
+
165
177
  context "when another worker has worked on a task since the job was found to be available, it" do
166
178
 
167
179
  before :each do
@@ -184,7 +196,7 @@ shared_examples_for 'a delayed_job backend' do
184
196
  it "should be the class name of the job that was enqueued" do
185
197
  described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
186
198
  end
187
-
199
+
188
200
  it "should be the method that will be called if its a performable method object" do
189
201
  job = described_class.new(:payload_object => NamedJob.new)
190
202
  job.name.should == 'named_job'
@@ -194,8 +206,15 @@ shared_examples_for 'a delayed_job backend' do
194
206
  @job = Story.create(:text => "...").delay.save
195
207
  @job.name.should == 'Story#save'
196
208
  end
209
+
210
+ it "should parse from handler on deserialization error" do
211
+ job = Story.create(:text => "...").delay.text
212
+ job.payload_object.object.destroy
213
+ job = described_class.find(job.id)
214
+ job.name.should == 'Delayed::PerformableMethod'
215
+ end
197
216
  end
198
-
217
+
199
218
  context "worker prioritization" do
200
219
  before(:each) do
201
220
  Delayed::Worker.max_priority = nil
@@ -206,7 +225,7 @@ shared_examples_for 'a delayed_job backend' do
206
225
  10.times { described_class.enqueue SimpleJob.new, rand(10) }
207
226
  jobs = described_class.find_available('worker', 10)
208
227
  jobs.size.should == 10
209
- jobs.each_cons(2) do |a, b|
228
+ jobs.each_cons(2) do |a, b|
210
229
  a.priority.should <= b.priority
211
230
  end
212
231
  end
@@ -227,23 +246,23 @@ shared_examples_for 'a delayed_job backend' do
227
246
  jobs.each {|job| job.priority.should <= max}
228
247
  end
229
248
  end
230
-
249
+
231
250
  context "clear_locks!" do
232
251
  before do
233
252
  @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
234
253
  end
235
-
254
+
236
255
  it "should clear locks for the given worker" do
237
256
  described_class.clear_locks!('worker')
238
257
  described_class.find_available('worker2', 5, 1.minute).should include(@job)
239
258
  end
240
-
259
+
241
260
  it "should not clear locks for other workers" do
242
261
  described_class.clear_locks!('worker1')
243
262
  described_class.find_available('worker1', 5, 1.minute).should_not include(@job)
244
263
  end
245
264
  end
246
-
265
+
247
266
  context "unlock" do
248
267
  before do
249
268
  @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
@@ -255,13 +274,13 @@ shared_examples_for 'a delayed_job backend' do
255
274
  @job.locked_at.should be_nil
256
275
  end
257
276
  end
258
-
277
+
259
278
  context "large handler" do
260
279
  before do
261
280
  text = "Lorem ipsum dolor sit amet. " * 1000
262
281
  @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
263
282
  end
264
-
283
+
265
284
  it "should have an id" do
266
285
  @job.id.should_not be_nil
267
286
  end
@@ -269,18 +288,19 @@ shared_examples_for 'a delayed_job backend' do
269
288
 
270
289
  describe "yaml serialization" do
271
290
  it "should reload changed attributes" do
272
- job = described_class.enqueue SimpleJob.new
273
- yaml = job.to_yaml
274
- job.priority = 99
275
- job.save
276
- YAML.load(yaml).priority.should == 99
291
+ story = Story.create(:text => 'hello')
292
+ job = story.delay.tell
293
+ story.update_attributes :text => 'goodbye'
294
+ described_class.find(job.id).payload_object.object.text.should == 'goodbye'
277
295
  end
278
296
 
279
- it "should ignore destroyed records" do
280
- job = described_class.enqueue SimpleJob.new
281
- yaml = job.to_yaml
282
- job.destroy
283
- lambda { YAML.load(yaml).should be_nil }.should_not raise_error
297
+ it "should raise deserialization error for destroyed records" do
298
+ story = Story.create(:text => 'hello')
299
+ job = story.delay.tell
300
+ story.destroy
301
+ lambda {
302
+ described_class.find(job.id).payload_object
303
+ }.should raise_error(Delayed::DeserializationError)
284
304
  end
285
305
  end
286
306
 
@@ -306,55 +326,15 @@ shared_examples_for 'a delayed_job backend' do
306
326
  Delayed::Worker.max_run_time = old_max_run_time
307
327
  end
308
328
  end
309
- end
310
-
311
- context "worker prioritization" do
312
- before(:each) do
313
- @worker = Delayed::Worker.new(:max_priority => 5, :min_priority => -5, :quiet => true)
314
- end
315
-
316
- it "should only work_off jobs that are >= min_priority" do
317
- create_job(:priority => -10)
318
- create_job(:priority => 0)
319
- @worker.work_off
320
-
321
- SimpleJob.runs.should == 1
322
- end
323
-
324
- it "should only work_off jobs that are <= max_priority" do
325
- create_job(:priority => 10)
326
- create_job(:priority => 0)
327
-
328
- @worker.work_off
329
-
330
- SimpleJob.runs.should == 1
331
- end
332
- end
333
329
 
334
- context "while running with locked and expired jobs" do
335
- before(:each) do
336
- @worker.name = 'worker1'
337
- end
338
-
339
- it "should not run jobs locked by another worker" do
340
- create_job(:locked_by => 'other_worker', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
341
- lambda { @worker.work_off }.should_not change { SimpleJob.runs }
342
- end
343
-
344
- it "should run open jobs" do
345
- create_job
346
- lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
347
- end
348
-
349
- it "should run expired jobs" do
350
- expired_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Worker.max_run_time)
351
- create_job(:locked_by => 'other_worker', :locked_at => expired_time)
352
- lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
353
- end
354
-
355
- it "should run own jobs" do
356
- create_job(:locked_by => @worker.name, :locked_at => (Delayed::Job.db_time_now - 1.minutes))
357
- lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
330
+ context "when the job raises a deserialization error" do
331
+ it "should mark the job as failed" do
332
+ Delayed::Worker.destroy_failed_jobs = false
333
+ job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
334
+ @worker.work_off
335
+ job.reload
336
+ job.failed_at.should_not be_nil
337
+ end
358
338
  end
359
339
  end
360
340
 
@@ -364,7 +344,7 @@ shared_examples_for 'a delayed_job backend' do
364
344
  Delayed::Worker.destroy_failed_jobs = true
365
345
  Delayed::Worker.max_attempts = 25
366
346
 
367
- @job = Delayed::Job.enqueue ErrorJob.new
347
+ @job = Delayed::Job.enqueue(ErrorJob.new)
368
348
  end
369
349
 
370
350
  it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
@@ -376,15 +356,32 @@ shared_examples_for 'a delayed_job backend' do
376
356
  @job.attempts.should == 1
377
357
  @job.failed_at.should_not be_nil
378
358
  end
379
-
359
+
380
360
  it "should re-schedule jobs after failing" do
381
- @worker.run(@job)
361
+ @worker.work_off
382
362
  @job.reload
383
363
  @job.last_error.should =~ /did not work/
384
364
  @job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
385
365
  @job.attempts.should == 1
386
366
  @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
387
367
  @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
368
+ @job.locked_by.should be_nil
369
+ @job.locked_at.should be_nil
370
+ end
371
+
372
+ it 'should re-schedule with handler provided time if present' do
373
+ @job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
374
+ @worker.run(@job)
375
+ @job.reload
376
+
377
+ (Delayed::Job.db_time_now + 99.minutes - @job.run_at).abs.should < 1
378
+ end
379
+
380
+ it "should not fail when the triggered error doesn't have a message" do
381
+ error_with_nil_message = StandardError.new
382
+ error_with_nil_message.stub!(:message).and_return nil
383
+ @job.stub!(:invoke_job).and_raise error_with_nil_message
384
+ lambda{@worker.run(@job)}.should_not raise_error
388
385
  end
389
386
  end
390
387
 
@@ -392,35 +389,35 @@ shared_examples_for 'a delayed_job backend' do
392
389
  before do
393
390
  @job = Delayed::Job.create :payload_object => SimpleJob.new
394
391
  end
395
-
392
+
396
393
  share_examples_for "any failure more than Worker.max_attempts times" do
397
- context "when the job's payload has an #on_permanent_failure hook" do
394
+ context "when the job's payload has a #failure hook" do
398
395
  before do
399
396
  @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
400
- @job.payload_object.should respond_to :on_permanent_failure
397
+ @job.payload_object.should respond_to :failure
401
398
  end
402
399
 
403
400
  it "should run that hook" do
404
- @job.payload_object.should_receive :on_permanent_failure
401
+ @job.payload_object.should_receive :failure
405
402
  Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
406
403
  end
407
404
  end
408
405
 
409
- context "when the job's payload has no #on_permanent_failure hook" do
410
- # It's a little tricky to test this in a straightforward way,
411
- # because putting a should_not_receive expectation on
412
- # @job.payload_object.on_permanent_failure makes that object
413
- # incorrectly return true to
414
- # payload_object.respond_to? :on_permanent_failure, which is what
415
- # reschedule uses to decide whether to call on_permanent_failure.
416
- # So instead, we just make sure that the payload_object as it
417
- # already stands doesn't respond_to? on_permanent_failure, then
406
+ context "when the job's payload has no #failure hook" do
407
+ # It's a little tricky to test this in a straightforward way,
408
+ # because putting a should_not_receive expectation on
409
+ # @job.payload_object.failure makes that object
410
+ # incorrectly return true to
411
+ # payload_object.respond_to? :failure, which is what
412
+ # reschedule uses to decide whether to call failure.
413
+ # So instead, we just make sure that the payload_object as it
414
+ # already stands doesn't respond_to? failure, then
418
415
  # shove it through the iterated reschedule loop and make sure we
419
416
  # don't get a NoMethodError (caused by calling that nonexistent
420
- # on_permanent_failure method).
421
-
417
+ # failure method).
418
+
422
419
  before do
423
- @job.payload_object.should_not respond_to(:on_permanent_failure)
420
+ @job.payload_object.should_not respond_to(:failure)
424
421
  end
425
422
 
426
423
  it "should not try to run that hook" do
@@ -442,18 +439,18 @@ shared_examples_for 'a delayed_job backend' do
442
439
  @job.should_receive(:destroy)
443
440
  Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
444
441
  end
445
-
442
+
446
443
  it "should not be destroyed if failed fewer than Worker.max_attempts times" do
447
444
  @job.should_not_receive(:destroy)
448
445
  (Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
449
446
  end
450
447
  end
451
-
448
+
452
449
  context "and we don't want to destroy jobs" do
453
450
  before do
454
451
  Delayed::Worker.destroy_failed_jobs = false
455
452
  end
456
-
453
+
457
454
  it_should_behave_like "any failure more than Worker.max_attempts times"
458
455
 
459
456
  it "should be failed if it failed more than Worker.max_attempts times" do