Rails 4.2 ActionMailer

From Rails 4.2, you can deliver an email asynchronously using ActiveJob which is built into ActionMailer.

ActionMailer now comes with two additional methods:

1 deliver_later
2 deliver_now

deliver_now is similar to the original deliver method; it sends the email synchronously.

deliver_later makes use of your background job system to send the email.

Note that there is a difference between the bang and non-bang methods.

With deliver_later! and its bang methods, no exceptions will be raised if an error occurs when sending the mail. Without the bang methods, exceptions will be raised if the mail fails to send.

To get this to work properly, some additional changes need to be made.

Firstly, make sure that your background job system is setup and working properly. We are using Sidekiq for this example.

This assumes that you have config.active_job.queue_adapter set like so:

1 # within config/application.rb
3 config.active_job.queue_adapter = :sidekiq

Next you need to add the ‘mailers’ queue into the config file. ActionMailer by default places background deliveries into this queue.

For sidekiq:

1 # config/sidekiq.yml
3 :queues:
4   - [mailers, 1]

Create a mailer and call deliver_later and if it all goes well you should see the below in your log file:

1 [ActiveJob] [ActionMailer::DeliveryJob] [fd25b472-9d80-47f2-9ea6-5da0e3cf7552] Performed ActionMailer::DeliveryJob from Sidekiq(mailers) in 3160.77ms

Tracking exceptions

It is inevitable that errors might occur in your mailers. When using sidekiq, it has a default mechanism for queuing and retrying failed jobs. If an exception occurs in your mailer when sending asynchronously, it will be placed into this queue and retried for at least 25 times as a default.

To be notified of when an exception occurs, we can hook into sidekiq eror handlers like so:

 1 # config/initializers/sidekiq.rb
 2 Sidekiq.configure_server do |config|
 3   config.error_handlers << Proc.new {|ex,ctx_hash|
 4     SidekiqError.log_exception(ex, ctx_hash)
 5   }
 6 end
 8 # sidekiq_error.rb
10 class SidekiqError
11   def self.logger
12     @logger ||= Logger.new("#{Rails.root}/log/sidekiq.log")
13   end
15   def self.log_exception(e, args={})
16     self.logger.error "Error class: #{args["error_class"]}"
17     self.logger.error "Error message: #{args["error_message"]}"
18     self.logger.error e.message
19     st = e.backtrace.join("\n")
21     self.logger.error st
22     self.logger.error "Failed At: #{args["failed_at"]}"
23     self.logger.error "Retry Count: #{args["retry_count"]}"
24     self.logger.error "Retried At: #{args["retried_at"]}"
25   end
26 end

I still have not worked out a way to rescue exceptions within async mailers as the class is defined automatically in ActionMailer when you call deliver_later from your mailer:

 1 # actionmailer/lib/action_mailer/delivery_job.rb
 3 require 'active_job'
 5 module ActionMailer
 6   # The <tt>ActionMailer::DeliveryJob</tt> class is used when you
 7   # want to send emails outside of the request-response cycle.
 8   class DeliveryJob < ActiveJob::Base # :nodoc:
 9     queue_as { ActionMailer::Base.deliver_later_queue_name }
11     def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
12       mailer.constantize.public_send(mail_method, *args).send(delivery_method)
13     end
14   end
15 end

One approach would be to create a refinement for ActionMailer::MessageDelivery enqueue_delivery private method to use a custom ActiveJob object which has a rescue_from block but that is the topic of another blog post.

Happy Hacking!