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
2
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
2
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
7
8 # sidekiq_error.rb
9
10 class SidekiqError
11 def self.logger
12 @logger ||= Logger.new("#{Rails.root}/log/sidekiq.log")
13 end
14
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")
20
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
2
3 require 'active_job'
4
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 }
10
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!