How to make ExceptionNotifier work with delayed_job in Rails 3?
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.
exception_notification for delayed_job
I've done something like this in the past for delayed job rake tasks:
require 'action_mailer'
class ExceptionMailer < ActionMailer::Base
def setup_mail
@from = ExceptionNotifier.sender_address
@sent_on = Time.now
@content_type = "text/plain"
end
def exception_message(subject, message)
setup_mail
@subject = subject
@recipients = ExceptionNotifier.exception_recipients
@body = message
end
end
namespace :jobs do
desc "sync the local database with the remote CMS"
task(:sync_cms => :environment) do
Resort.sync_all!
result = Delayed::Job.work_off
unless result[1].zero?
ExceptionMailer.deliver_exception_message("[SYNC CMS] Error syncing CMS id: #{Delayed::Job.last.id}", Delayed::Job.last.last_error)
end
end
end
Configuring Exception Notification with Delayed Jobs in Rails 4
Since exception_notification 4.0 there's a new way to handle manual notify. Try to change
ExceptionNotifier::Notifier.background_exception_notification(error)
for
ExceptionNotifier.notify_exception(error)
Requeue or evaluate delayed job from inside?
As you've said, if the Delayed Job runner gets to the end of the perform
queue then it will be considered as a successful run and removed from the queue. So you just have to stop it from getting to the end. There isn't a requeue -- even if there was it'd be a new record with new attributes. So you may rethink whatever it is that is causing the job to notify you about exceptions. You could, for example, add a condition upon which to notify you...
Potential Solutions
You can get the default JOBS_MAX
(as you pseudo-coded it) with Delayed::Worker.max_attempts
or you can set your own per-job by defining a method, e.g.: max_attempts
.
# Fail permanently after the 10th failure for this job
def max_attempts
10
end
That is, this method will be usable given the following:
You can also make use of callback hooks. Delayed Job will callback to your payload object via the error
method if it is defined. So you may be able to use the error
method to notify you of actual exceptions beyond a given attempt number. To do that...
Within the callback, the Delayed::Job object itself is returned as the first argument:
def error(job, exception)
job.attempts # gives you the current attempt number
# If job.attempts is greater than max_attempts then send exception notification
# or whatever you want here...
end
So you can use the callbacks to start adding logic on when to notify yourself and when not to. I might even suggest making a base set of functionality that you can include into all of your payload objects to do these things... but that's up to you and your design.
Rails exception notifier in rake tasks
Airbrake gem patches Rake to allow rescuing, so it already does what I'm asking...
ActionMailer::Base::NullMail when trying exception_notification in development
Likely you are using an old version of the ExceptionNotifier and a newer version of ActiveMailer::Base. Not calling the mail command within the email functionality will result in the ActionMailer::Base::NullMail instance returned rather than a Mail instance.
From documentation:
class Notifier < ActionMailer::Base
default :from => 'no-reply@example.com',
:return_path => 'system@example.com'
def welcome(recipient)
@account = recipient
mail(:to => recipient.email_address_with_name,
:bcc => ["bcc@example.com", "Order Watcher <watcher@example.com>"])
end
end
401 unauthorized errors on Heroku with Delayed Job
Okay, I figured this out by - of course - browsing the source of the various libraries that were invoked in the backtrace.
The most relevant line:
vendor/bundle/ruby/1.9.1/gems/heroku-api-0.3.5/lib/heroku/api/processes.rb:9:in `get_ps'
This uses the heroku-api gem to go get some worker processes. Browsing the source of the heroku-api gem, I see that when you make a request via the API, it initializes a connection like so:
@api_key = options.delete(:api_key) || ENV['HEROKU_API_KEY']
if !@api_key && options.has_key?(:username) && options.has_key?(:password)
@connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options.merge(:headers => HEADERS))
@api_key = self.post_login(options[:username], options[:password]).body["api_key"]
end
My environment did not have ENV['HEROKU_API_KEY']
set. I did have the HEROKU_PASSWORD
set, which contains the same data, but that's not what this gem is looking for. Thus, I was getting 401 errors on this.
I will submit an update to the Heroku documentation and ask them to include this as one of the steps, since it is not in there now.
To fix this, I simply did:
heroku config:add HEROKU_API_KEY=KEY
Where KEY is the same as the value for HEROKU_PASSWORD. (You can view all config vars with heroku config
).
Related Topics
How to Use Mongodb Ruby Driver to Do a "Group" (Group By)
Expected #Count to Have Changed by 1, But Was Not Given a Block
Strong Parameters Not Accepting Array
Telling Bundler to Exclude Certain Gems from a Particular Gem's Installation
Ruby Tcpsocket: Find Out How Much Data Is Available
Drop-Down-Menu for Many-To-Many Relation in Rails Using Nested Attributes
Is There an Equivalent of Array#Find_Index for the Last Index in Ruby
Bundle Install/Error in Installing Libv8 (3.3.10.4) on Rails (Running on Lion)
What's the Difference Between /\P{Alpha}/I and /\P{L}/I in Ruby
How to Understand Sender and Receiver in Ruby
Use Rspec's "Expect" etc. Outside a Describe ... It Block
Multiple Servers in a Single Eventmachine Reactor
Rails 3 Actionmail Openssl::Ssl::Sslerror
Best Practices in Ruby for Loop
Graphql::Client::Dynamicqueryerror Expected Definition to Be Assigned to a Static Constant
How to Override Gemfile for Local Development