26

I'd like ExceptionNotifier to send out an email when an exception happens in a delayed job, just like for other exceptions. How can I achieve that?

abhas
  • 5,193
  • 1
  • 32
  • 56
Alex Korban
  • 14,916
  • 5
  • 44
  • 55
  • Interesting question, in looking I came across http://stackoverflow.com/questions/4104093/exception-notification-for-delayed-job and http://groups.google.com/group/delayed_job/browse_thread/thread/646314145a69360b?fwc=1&pli=1 – William May 12 '11 at 03:24
  • Yep, I've seen both of those but I think these solutions only work with Rails 2. – Alex Korban May 12 '11 at 03:40

6 Answers6

23

I do this with Rails 3.2.6, delayed_job 3.0.3 and exception_notification 2.6.1 gem

# In config/environments/production.rb or config/initializers/delayed_job.rb

# Optional but recommended for less future surprises.
# Fail at startup if method does not exist instead of later in a background job 
[[ExceptionNotifier::Notifier, :background_exception_notification]].each do |object, method_name|
  raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
end

# Chain delayed job's handle_failed_job method to do exception notification
Delayed::Worker.class_eval do 
  def handle_failed_job_with_notification(job, error)
    handle_failed_job_without_notification(job, error)
    # only actually send mail in production
    if Rails.env.production?
      # rescue if ExceptionNotifier fails for some reason
      begin
        ExceptionNotifier::Notifier.background_exception_notification(error)
      rescue Exception => e
        Rails.logger.error "ExceptionNotifier failed: #{e.class.name}: #{e.message}"
        e.backtrace.each do |f|
          Rails.logger.error "  #{f}"
        end
        Rails.logger.flush
      end
    end
  end 
  alias_method_chain :handle_failed_job, :notification 
end

It's probably a good idea to load this code in all environments to catch errors after bundle update etc before they reach production. I do this by having a config/initializers/delayed_job.rb file but you could duplicate the code for each config/environments/* environment.

Another tip is to tune the delayed job config a bit as default you may get a lot of duplicate exception mails when job fails.

# In config/initializers/delayed_job_config.rb
Delayed::Worker.max_attempts = 3

Update I had some problems with the delayed_job daemon silently exiting and it turned out to be when ExceptionNotifier fails to send mail and no one rescued the exception. Now the code rescues and log them.

Mattias Wadman
  • 11,172
  • 2
  • 42
  • 57
  • 1
    I'd recommend using the official exception notification gem at https://github.com/smartinez87/exception_notification – Christopher Manning Dec 27 '11 at 03:13
  • Ah ok, will update to that. I was using the rails3 gem as there seamd to be some rails 3 issues with the official version at the time. – Mattias Wadman Dec 27 '11 at 14:47
  • +1 This approach works with Rails 3.2.6, Delayed Job 3.0.3, and Exception Notification 2.6.1. Initially I thought adding an error method using the built-in hook would be a cleaner approach, but that would have to be defined for each job class, and defining it generically for `PerformableMethod` didn't work due to how `delegate` is used. – Nathan Jul 07 '12 at 20:04
  • Nice. I actually updated to the same versions the other day but didn't remember to update the answer. – Mattias Wadman Jul 09 '12 at 11:13
  • 3
    I've added a ```, :data => {:job => job}``` to the method call so that I have a bit more details… – mat Mar 11 '13 at 17:03
  • 1
    To get notified ONLY when last attempt failed I add: `ExceptionNotifier.notify_exception(error) if job.attempts == Delayed::Worker.max_attempts` in place of: `ExceptionNotifier::Notifier.background_exception_notification(error)` – micred Oct 22 '15 at 12:42
6

Adding to @MattiasWadman answer, since exception_notification 4.0 there's a new way to handle manual notify. So instead of:

ExceptionNotifier::Notifier.background_exception_notification(error)

use

ExceptionNotifier.notify_exception(error)
Alter Lagos
  • 12,090
  • 1
  • 70
  • 92
4

Another way to handle exceptions (put as an initializer):

class DelayedErrorHandler < Delayed::Plugin

  callbacks do |lifecycle|

    lifecycle.around(:invoke_job) do |job, *args, &block|

      begin
        block.call(job, *args)
      rescue Exception => e

        # ...Process exception here...

        raise e
      end
    end
  end
end

Delayed::Worker.plugins << DelayedErrorHandler
Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
  • I think this is cleanest solution. BTW other solutions wont work in the future, because alias_method_chain is deprecated in Rails 5 – remo Feb 02 '17 at 14:37
3

alias_method_chain no longer exists in Rails 5.

Here's the new (proper) way to do this using Ruby 2's prepend

# In config/initializers/delayed_job.rb
module CustomFailedJob
  def handle_failed_job(job, error)
    super
    ExceptionNotifier.notify_exception(error, data: {job: job})
  end
end

class Delayed::Worker
  prepend CustomFailedJob
end
akaspick
  • 1,541
  • 19
  • 17
2

For exception_notification 3.0.0 change:

ExceptionNotifier::Notifier.background_exception_notification(error)

to:

ExceptionNotifier::Notifier.background_exception_notification(error).deliver
Marijn
  • 10,367
  • 5
  • 59
  • 80
Adrian
  • 21
  • 1
0

simpler and updated answer:

# Chain delayed job's handle_failed_job method to do exception notification
Delayed::Worker.class_eval do
  def handle_failed_job_with_notification job, error
    handle_failed_job_without_notification job, error
    ExceptionNotifier.notify_exception error,
      data: {job: job, handler: job.handler} rescue nil
  end 
  alias_method_chain :handle_failed_job, :notification
end

And test on console with:

Delayed::Job.enqueue (JS=Struct.new(:a){ def perform; raise 'here'; end }).new(1)
brauliobo
  • 5,843
  • 4
  • 29
  • 34