Skip to content

Commit

Permalink
Added on_permanent_failure hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Phil Darnowsky committed Apr 6, 2010
1 parent 54d4091 commit d2f14cd
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.gem
*.swp
14 changes: 14 additions & 0 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ end
Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))
</pre>

You can also add an optional on_permanent_failure method which will run if the job has failed too many times to be retried:

<pre>
class ParanoidNewsletterJob < NewsletterJob
def perform
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
end

def on_permanent_failure
page_sysadmin_in_the_middle_of_the_night
end
end
</pre>

h2. Gory Details

The library evolves around a delayed_jobs table which looks as follows:
Expand Down
6 changes: 6 additions & 0 deletions lib/delayed/worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ def reschedule(job, time = nil)
job.save!
else
say "* [JOB] PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO

if job.payload_object.respond_to? :on_permanent_failure
say "* [JOB] Running on_permanent_failure hook"
job.payload_object.on_permanent_failure
end

self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
end
end
Expand Down
9 changes: 8 additions & 1 deletion spec/sample_jobs.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'ruby-debug'

class SimpleJob
cattr_accessor :runs; self.runs = 0
def perform; @@runs += 1; end
Expand All @@ -12,10 +14,15 @@ class LongRunningJob
def perform; sleep 250; end
end

class OnPermanentFailureJob < SimpleJob
def on_permanent_failure
end
end

module M
class ModuleJob
cattr_accessor :runs; self.runs = 0
def perform; @@runs += 1; end
end

end
end
48 changes: 45 additions & 3 deletions spec/worker_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'spec_helper'
require 'ruby-debug'

describe Delayed::Worker do
def job_create(opts = {})
Expand Down Expand Up @@ -134,12 +135,52 @@ def job_create(opts = {})
before do
@job = Delayed::Job.create :payload_object => SimpleJob.new
end


share_examples_for "any failure more than Worker.max_attempts times" do
context "when the job's payload has an #on_permanent_failure hook" do
before do
@job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
@job.payload_object.should respond_to :on_permanent_failure
end

it "should run that hook" do
@job.payload_object.should_receive :on_permanent_failure
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
end
end

context "when the job's payload has no #on_permanent_failure hook" do
# It's a little tricky to test this in a straightforward way,
# because putting a should_not_receive expectation on
# @job.payload_object.on_permanent_failure makes that object
# incorrectly return true to
# payload_object.respond_to? :on_permanent_failure, which is what
# reschedule uses to decide whether to call on_permanent_failure.
# So instead, we just make sure that the payload_object as it
# already stands doesn't respond_to? on_permanent_failure, then
# shove it through the iterated reschedule loop and make sure we
# don't get a NoMethodError (caused by calling that nonexistent
# on_permanent_failure method).

before do
@job.payload_object.should_not respond_to(:on_permanent_failure)
end

it "should not try to run that hook" do
lambda do
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
end.should_not raise_exception(NoMethodError)
end
end
end

context "and we want to destroy jobs" do
before do
Delayed::Worker.destroy_failed_jobs = true
end


it_should_behave_like "any failure more than Worker.max_attempts times"

it "should be destroyed if it failed more than Worker.max_attempts times" do
@job.should_receive(:destroy)
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
Expand All @@ -156,6 +197,8 @@ def job_create(opts = {})
Delayed::Worker.destroy_failed_jobs = false
end

it_should_behave_like "any failure more than Worker.max_attempts times"

it "should be failed if it failed more than Worker.max_attempts times" do
@job.reload.failed_at.should == nil
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
Expand All @@ -166,7 +209,6 @@ def job_create(opts = {})
(Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
@job.reload.failed_at.should == nil
end

end
end
end
Expand Down

0 comments on commit d2f14cd

Please sign in to comment.