【rails】delayed_jobを使って非同期化をする方法

ことの始まり

現在運用しているサービスは、メール送信に失敗した際Bugsnagで通知を飛ばすようにしていた。
通知を受けたら手動でメールの再送を行っていたが、利用者数が増えるにつれてエラーもかなりの数発生するようになってきて、手動で対応するのが辛くなってきた。
もうこれは非同期化するしかない!
というわけで、イチから調べて実装することにした。

delayed_jobの導入

非同期化をするには様々な手法があるけど、よくあるdelayed_jobを使うことにした。
まずはGemfileに追加する。

gem 'delayed_job_active_record'
gem 'daemons'

demonsは実際にサーバーで稼働させるのに必要なgem。
delayed_jobはDBを使って動くので、以下のコマンドでテーブルを作ってあげる。

$ bundle install
$ bundle exec rails generate delayed_job:active_record
$ bundle exec rake db:migrate

続いてconfig/application.rbdelayed_jobのキューを登録する。
今回はActive Jobを使うのでdelayed_jobにすれば良い。

class Application < Rails::Application
  config.active_job.queue_adapter = :delayed_job
end

これでdelayed_jobが導入できた。

delayed_jobの実装

例えばorder_mailer.rbsendメソッドでメールを送信する場合、controller側はこんなコードになっている。

class OrdersController < ApplicationController
  def  submit
    ~~~  #ここにはorderを保存する処理とかがいるはず
    # メールを送る処理
    OrderMailer.send(order).deliver
  end
end

これを非同期化させるには以下のようにすれば良い

  def submit
    OrderMailer.send(order).deliver_later
  end

delayed_jobを使った繰り返し処理

deliver_laterを使うことで非同期化はできたものの、今のままだと1回失敗したらメールが送られないままになってしまう。
なので、以下の2つを実装していくことにする。

  • 1回失敗しても何回かリトライ。
  • 最終的に失敗した場合は通知。

そのためにはジョブファイルを作る必要がある。
今回はorderメールのジョブなので、order_mail_deliver_jobとかにしておく。

$ rails g job order_mail_deliver
create spec/jobs/order_mail_deliver_job_spec.rb
create app/jobs/application_job.rb  # これは最初だけ
create app/jobs/order_mail_deliver_job.rb

まずはorder_mail_deliver_job.rbの中身から実装していく。
上で作ったメールを送る処理をこの中に移動。

class OrderMailDeliverJob < ApplicationJob
  queue_as :default

  def perform(order)
    OrderMailer.send(order).deliver_now  # ジョブに登録されるのでdeliver_nowを使う
  end
end

controller側に作ったコードを適用しておく。

  def submit
    OrderMailDeliverJob.perform_later(order)  # 非同期化なのでperform_laterを使う
  end

続いてリトライ機能を作成する。
ruby 5.1以上であればretry_onを使えば楽なのだが、今回はruby 5.0だったので自分で設定が必要だった…
他のジョブを作った場合も考えてapplication_job.rbにリトライ機能を実装する。

class ApplicationJob
  attr_reader :attempt_number  # 実行回数確認用の変数

  RETRY_LIMIT = 3  # リトライさせたい回数

  rescue_from(StandardError) do |e|
    if retry_limit_exceeded?  # リトライ回数上限を超えたら通知
      Bugsnag.notify(e)
    else  # 上限以下ならリトライ
      retry_job(wait: wait_time)  # 何秒後にリトライするかをwaitに記載
    end
  end

  def serialize
    super.merge("attempt_number" => (@attempt_number || 0) + 1)
  end

  def deserialize(job_data)
    super
    @attempt_number = job_data["attempt_number"]
  end

  private

  def retry_limit_exceeded?
    @attempt_number >= RETRY_LIMIT
  end

  def wait_time
    attempt_number**2
  end
end

分割する必要もないので、Bugsnagへの通知もまとめて書いてしまった。

ローカル環境で動作確認

delayed_jobをローカルで確認するにはワーカーを起動させる必要がある。

bin/rake jobs:work

Elastic Beanstalk環境でワーカーの起動

みなさん大好きELB環境ではdeployと同時にワーカーの起動が必要になる。
そのためには..ebextensionsにワーカー起動用のconfigファイルを追加する必要がある。

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/post/99_restart_delayed_job.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      # Using similar syntax as the appdeploy pre hooks that is managed by AWS

      # Loading environment data
      EB_SCRIPT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k script_dir)
      EB_SUPPORT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k support_dir)
      EB_APP_USER=$(/opt/elasticbeanstalk/bin/get-config container -k app_user)
      EB_APP_CURRENT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_deploy_dir)
      EB_APP_PIDS_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_pid_dir)

      # Setting up correct environment and ruby version so that bundle can load all gems
      . $EB_SUPPORT_DIR/envvars
      . $EB_SCRIPT_DIR/use-app-ruby.sh

      # Now we can do the actual restart of the worker. Make sure to have double quotes when using env vars in the command.
      cd $EB_APP_CURRENT_DIR
      su -s /bin/bash -c "bin/delayed_job -n 2 --pid-dir=$EB_APP_PIDS_DIR restart" $EB_APP_USER

一番最後のコードで-n 2としているのはワーカーを2つ起動させるオプション。

サーバー上のワーカー起動確認

ジョブのワーカーが動いているかは配布した環境で以下のコードを叩く必要がある。
叩いて何かしら出てきていれば無事ワーカーが稼働していることがわかる。

$ ps aux | grep -v grep | grep delay
app ~~~

まとめ

これでdelayed_jobを無事導入することができた。
初めてだったので環境配布は特にビビったけど、問題なく動いてくれた。
メール以外にもファイルのアップロードとかは非同期化ができるので、使い道はひろそうな予感。
なお、導入よりもrspecの方が100倍苦労したけど…長くなったのでそれは次の記事でまとめることにする。