🥰

Action Mailer/Active job/Sidekiq/Redisを用いて時間指定した非同期なメール送信アプリの作成

2023/02/19に公開

背景

railsでメール送信を時間指定して送信してみたかったので、Action Mailerでメール機能、Active job/Sidekiq/Redisでバックグラウンドで非同期な処理を実装してみました。

今後、多くの方がバックグラウンドで時間指定した重い処理などを実装するときに役に立つのではないかと思ったので、今回の記事を作成しました。

作成したアプリの機能は、メールの差出人、受取人、メールタイトル、メール内容、送信時間の指定などを設定することができ、指定した時間が来たら、メール送信が行われるといった機能となっております。

送信された内容は、mailcatcherというライブラリを用いて確認しています。

(Action Mailerにプレビュー機能があるので、メールの内容を確認したいだけであればそちらでも確認できます。今回は、メールの内容とメール内容を登録したとき、指定した時間にメールが送信されるか確認したかったので、mailcatcherを使用しました。)

完成動画とソースコード(READMEにER図を簡単に載せたので、今回作成したモデルやデータベースの構造などよかったら参考にしてみてください。)

完成図

参考記事

Action Mailer の基礎 - Railsガイド

Active Job の基礎 - Railsガイド

https://github.com/sidekiq/sidekiq

【Rails7】Redis+Sidekiq+Active Jobで指定した日時に非同期処理を行う

Active Job、Sidekiq、Redisの関係性と使用理由

まずは、Active Job、Sidekiq、Redisの関係性を見ていき、なぜ使用すべきなのか解説していきます。(この記事が非常にわかりやすかったです。)

少し解説すると、Active Job, Sidekiq, and Redis は、Ruby on Rails アプリケーションでのバックグラウンドジョブ処理に関連する技術です。

Active Job は、Ruby on Rails のフレームワークが提供するバックグラウンドジョブ管理システムです。Active Job は、多数のバックエンド(例えば Sidekiq、Resque、Delayed Jobなど)から選択して使用することができます。ジョブ(Job)は、要するに実行する処理のこと。

Sidekiqは、Rubyで書かれたバックグラウンドジョブ管理システムです。Sidekiqは、高速な処理と高いスケーラビリティ(拡張性)を提供することで知られています。

Redisは、高速なキー・バリューストアです。Sidekiq は Redis にジョブを保存し、処理するために使用します。Redis は、ジョブをキャッシュするために使用されます。これにより、Sidekiqは、高速なジョブ処理を行うことができます。

これらの3つの技術は、Ruby on Rails アプリケーションでのバックグラウンドジョブ処理を効率的かつスケーラブルにすることができます。

使用する流れとしては、まず、Active Jobで実行する処理であるJobの作成とキューというデータ構造でJobを登録するという処理を行います。

次に、RedisでJobを永続化させるためJobをデータとして管理し、Active Jobで作成したJobを追加(エンキュー)します。

最後にSidekiqでRedisで管理しているJobを取り出し(デキュー)、そのJobを実行します。

Active Job, Sidekiq, Redisの使用理由は、重い処理やRailsの処理とは別に走らせたい処理などを非同期に実行させ効率化を図るためです。

時間指定したメール送信の実装

いざ実装するとなった時、どこから実装していくべきか悩みましたが、Sidekiq,Redis, Active Jobの設定はこの記事を参考にし、非同期処理を実装してみてください。

(実装した非同期処理が bundle exec sidekiq コマンドを実行したsidekiqのログが現れるターミナルで、非同期処理が実行されたことを確認してから下記の時間指定したメール送信処理を実行する方法を参考に時間指定したメール送信処理を実装する方法を見ていただけると幸いです。)

今回は、上記3つの非同期処理を実行するためのライブラリと Action Mailerを使用し、時間指定したメール送信の実装方法を解説していきます。

実装手順

①Action MailerでMailerを作成し、メール送信処理を作成

②Active JobでJobの中にメール送信処理を記述し、Jobを作成

③メールの送信者や受信者のデータを作成するときに作成したJobの処理を実行する処理を記述

①Action MailerでMailerを作成し、メール送信処理を作成

Email メーラーを作成

bin/rails generate mailer Email  

実行結果

app/mailers/email_mailer.rb

class EmailMailer < ApplicationMailer
end

メール送信するためのsend_mailメソッドを定義し、引数にemailというメール登録時のデータを設定

app/mailers/email_mailer.rb

class EmailMailer < ApplicationMailer
  def send_mail(email)
    # @emailにはメール内容登録時のデータ(email)を格納
    @email = email

    # @emailのデータからメールの送信者や受信者、メールタイトルをメール送信時に反映
    mail(
      from: @email.from,
      to:   @email.to,
      subject: 'メールきたよ〜'
    )
  end
end

送信時のメールテキスト内容(send_mailメソッドが実行されると下記のテキスト内容がメールとして送信される。)

app/views/email_mailer/send_mail.text.erb

<%= @email.from %> からメールきたで。

◯メール内容
<%= @email.body %>

②Active JobでJobの中にメール送信処理を記述し、Jobを作成

app/jobs/send_email_job.rb

class SendEmailJob < ApplicationJob
  queue_as :default

  def perform(email)
    # ターミナルでsidekiqで設定した非同期処理が実行されたかのlogを確認するためテキスト表示(putsの中は、bundle exec sidekiqを実行したターミナルで確認できます。)
    puts 'メールきたで〜〜〜〜'
    puts 'メールタイトル:' + email.subject

    # メール送信処理をJobに設定・Job内で実行
    EmailMailer.send_mail(email).deliver_later
  end
end

メール送信処理には、deliver_nowとdeliver_laterの2種類あり、どちらもメールを送信するためのメソッドです。

今回は、メール送信の処理が特に重くなかったため、deliver_now、deliver_laterを用いてもメール送信処理を実行することができました。

ただ、公式ドキュメントによると、deliver_laterはメール送信を非同期に行うので、メール送信処理が重い時、メール送信が完了しなくてもメール送信処理以降の処理を継続して行うことができます。

一方、メール送信処理を同期的に行うdeliver_nowはメール送信処理が重い時はその処理が完了するまで待たなければならず、メール送信処理が完了するまで、その処理以降の処理を行うことができません。

deliver_nowとdeliver_laterの違い

参考1

参考2

今回は、このようなことも考慮したため、deliver_laterメソッドで非同期的にメール送信処理を実行させました。

(さらにdeliver_laterにはdeliver_later(wait: 1.hour)deliver_later(wait_until: 10.hours.from_now)のように、実行時間を遅らせることができます。

例えば、今回deliver_later(wait: 1.hour)を用いたら、「指定時間 + 1時間後」にメール送信処理が行われるということになります。)

③メールの送信者や受信者のデータを作成するときに作成したJobの処理を実行する処理を記述

app/controllers/emails_controller.rb

class EmailsController < ApplicationController
  before_action :set_email, only: %i[ show edit update destroy ]

  ---省略---

  # メール内容の作成
  def create
    @email = Email.new(email_params)

    respond_to do |format|
      if @email.save

        # メール作成時にメール送信処理のJobを実行
        SendEmailJob.set(wait_until: @email.sent_at).perform_later(@email)

        format.html { redirect_to email_url(@email), notice: "Email was successfully created." }
        format.json { render :show, status: :created, location: @email }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @email.errors, status: :unprocessable_entity }
      end
    end
  end

  # メール内容の更新
  def update
    respond_to do |format|
      if @email.update(email_params)

        # メール更新時にメール送信処理のJobを実行
        SendEmailJob.set(wait_until: @email.sent_at).perform_later(@email)
        
        format.html { redirect_to email_url(@email), notice: "Email was successfully updated." }
        format.json { render :show, status: :ok, location: @email }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @email.errors, status: :unprocessable_entity }
      end
    end
  end

  ----省略---

  private
    def set_email
      @email = Email.find(params[:id])
    end

    def email_params
      params.require(:email).permit(:to, :from, :subject, :body, :sent_at)
    end
end

今回最も重要な部分は上記のこの部分です。

# メール作成時にメール送信処理のJobを実行
SendEmailJob.set(wait_until: @email.sent_at).perform_later(@email)

先ほどsend_email_job.rbで作成したSendEmailJobに対して、set(wait_until: @email.sent_at)を実行し送信時間を指定しました。

さらに、perform_later(@email)とすることで、send_email_job.rbで作成したperform(email)メソッドを実行し、引数に@emailという作成したメールの内容を設定し、performメソッドの引数として渡しています。

このperform_laterメソッドにより、作成したJobをキューに登録しています。

公式ドキュメント参照)

上記のような形で実装することで時間指定したメール送信処理を実行することができました。

今回は時間指定した非同期なメール送信処理をご紹介いたしましたが、今後はLINEやSlackと連携させ時間指定したメッセージなどを送信させるリマインダー機能を作成できたらいいなと思います。

Discussion