Skip to content

Instantly share code, notes, and snippets.

@maxivak
Last active March 9, 2023 06:55
Embed
What would you like to do?
Sending emails with ActionMailer and Sidekiq

Sending emails with ActionMailer and Sidekiq

Send email asynchroniously using Sidekiq.

ActionMailer

Create your mailer us usual:

# app/mailers/users_mailer.rb

class UsersMailer < ActionMailer::Base

def welcome_email(user_id)
    @user = User.find(user_id)

    mail(   :to      => @user.email,
            :subject => "Welcome"
    ) do |format|
      format.text
      format.html
    end
  end
end

Views for email:

app/views/users_mailer/welcome_email.html.erb - HTML version
app/views/users_mailer/welcome_email.text.erb - TEXT version

Send email:

user = User.find(1)

mail = UsersMailer.welcome_email(user.id)
#mail.deliver_now
mail.deliver_later

    

Sidekiq

Gemfile:

gem 'sidekiq'

Install Redis

Redis provides data storage for Sidekiq. It holds all the job data along with runtime and historical data

Configure Sidekiq

To make #deliver_later work we need to tell ActiveJob to use Sidekiq. As long as Active Job is setup to use Sidekiq we can use #deliver_later.

# config/initializers/active_job.rb
  
  config.active_job.queue_adapter = :sidekiq

Environment file:

# config/environments/development.rb

Rails.application.configure do
  ...
  
  config.active_job.queue_adapter = :sidekiq
  
  config.action_mailer.perform_deliveries = true
  config.action_mailer.raise_delivery_errors = true

  config.action_mailer.delivery_method = :smtp
  
  config.action_mailer.smtp_settings = { ... }
  

end

Read more about ActionJob and Sidekiq: https://github.com/mperham/sidekiq/wiki/Active-Job

Configure Sidekiq

config/sidekiq.yml:

---
:concurrency: 1
:queues:
  - default
  - mailers

Specify Redis namespace for different environments:

# config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" }
end

Sidekiq.configure_client do |config|
  config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" }
end

Sidekiq and ActionMailer (ActionJob)

By default, jobs to deliver emails will be placed in queue named :mailers. To change queue name for ActionMailer use this config:

# config/environments/development.rb

  config.active_job.queue_adapter = :sidekiq

  config.active_job.queue_name_prefix = "mysite"
  config.active_job.queue_name_delimiter = "_"

This will use queues named :mysite_mailers, etc.

!!! important. You may need to include new queue names in sidekiq.yml file:

# config/sidekiq.yml

---
:concurrency: 1
:queues:
  - default
  - mailers

  - mysite_default
  - mysite_mailers

Sidekiq and Devise

Read this: https://github.com/mperham/sidekiq/wiki/Devise

Run Sidekiq

bundle exec sidekiq --environment development -C config/sidekiq.yml 

God + Sidekiq

Use god for monitoring and running sidekiq automatically: https://gist.github.com/maxivak/05847dc7f558d5ef282e

RSpec tests

RSpec tests for ActionMailer

In these tests we do not use Sidekiq.

test environment:

# config/environments/test.rb

Rails.application.configure do
 ...
 
  config.active_job.queue_adapter = :test

  config.action_mailer.perform_deliveries = true
  config.action_mailer.delivery_method = :test
  config.action_mailer.raise_delivery_errors = true

 
 

Test that deliver_later method was called:

user = User.first

    #
    message_delivery = instance_double(ActionMailer::MessageDelivery)
    expect(UsersMailer).to receive(:welcome_email).with(user.id).and_return(message_delivery)
    expect(message_delivery).to receive(:deliver_later)
    

#   
mail = UsersMailer.welcome_email(user.email)
mail.deliver_later
    
    

RSpec tests for Sidekiq

test environment:

# config/environments/test.rb

Rails.application.configure do
 ...
   
  config.active_job.queue_adapter = :sidekiq

  config.action_mailer.perform_deliveries = true
  config.action_mailer.delivery_method = :test
  config.action_mailer.raise_delivery_errors = true

 

spec/rails_helper.rb:

# sidekiq
require 'sidekiq/testing'
Sidekiq::Testing.fake!  # by default it is fake

User Sidekiq::Worker.jobs.size to see the number of jobs in the queue.

Test that email was enqueued:


RSpec.describe "Test sending email with sidekiq", :type => :request do

  it 'send email to sidekiq' do

    user = User.first
      
    expect{
      UsersMailer.welcome_email(user.id).deliver_later
    }.to change( Sidekiq::Worker.jobs, :size ).by(1)

  end

end

@darrenterhune
Copy link

@SeriusDavid deliver_now http://api.rubyonrails.org/classes/ActionMailer/MessageDelivery.html#method-i-deliver_now

@r4mbo7 not possible with active_job, you would have to create a sidekiq job to be able to use the sidekiq advanced options like retry etc.

@mc2dx
Copy link

mc2dx commented Oct 1, 2019

👍

@matochondrion
Copy link

This guide is extremely helpful, thank you!

I wanted to mention an issue I had when testing if the email was enqueued with Sidekick - in case it helps anyone else. Using argument syntax with change rather than block syntax wasn't working for me. The value of .size didn't change.

RSpec.describe "Test sending email with sidekiq", :type => :request do
  it 'send email to sidekiq' do
    user = User.first
      
    expect{
      UsersMailer.welcome_email(user.id).deliver_later
    }.to change(Sidekiq::Worker.jobs, :size).by(1) # `size` returns `0`
  end
end

I believe the reason is that with argument syntax, Sidekiq::Worker.jobs is evaluated before the expect runs, and it returns a different array instance each time it's executed.

So checking .size in this case was returning the size of the jobs instance before the expect block executes.

Changing to a block syntax so that .jobs is evaluated both before and after the expect block solved my issue:

RSpec.describe "Test sending email with sidekiq", :type => :request do
  it 'send email to sidekiq' do
    user = User.first
      
    expect{
      UsersMailer.welcome_email(user.id).deliver_later
    }.to change { Sidekiq::Worker.jobs.size }.by(1) # `size` returns `1`
  end
end

@rgaufman
Copy link

Is there anyway to make sidekiq retry sending the email 5 times before throwing an exception? - I currently get 2-3 exceptions per day that are:

Net::ReadTimeoutActionMailer::MailDeliveryJob@default
gems/net-protocol-0.1.3/lib/net/protocol.rb:219 rbuf_fill

They seem harmless and happen when Google servers are busy, a retry fixes it but they are clocking up Bugsnag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment