2021/9/30追記 sidekiqのdaemon化について
###はじめに
Railsでアプリケーションを開発中、500件のレコードを作成し、各レコードに対してメソッドを実行していたところ、レコードが二重で作成されたり、メソッドがうまく実行されていないなど予期せぬ挙動を起こしました。原因を完全に突き止めることは出来ませんでしたが、おそらく処理が重たくなり、リクエストが二重で発生してしまったのではないかと考えています。
そこで、一連の処理をActive Jobに任せ、バックグラウンドで実行する実装に変更しました。
キューやジョブなど概念として知っていた内容を初めて実装に紐づけることができましたので、合わせてまとめたいと思います。
###Active Jobについて
####概要
Active JobとはRails標準のジョブを処理するフレームワークです。このフレームワークを利用することで、キュー操作をバックエンドで実行することができるようになります。キューイングシステムが空いたらジョブを実行する、指定した時間になったらジョブを実行するなどの指定も可能です。
####キューとジョブとは
簡単に説明するとキューという枠にジョブという単位の箱を入れていき、先に入れたものから先に取り出され実行されるというものです。
####キューイングシステム
- インプロセスのキューイングシステム
- ジョブをメモリに保持する
- プロセスがクラッシュしたりコンピュータをリセットするとジョブが失われる
- Rails標準搭載
- アウトオブプロセスのキューイングシステム
- 別のプロセスを立ててキューを管理する
- Active Jobには様々なキューイングバックエンドに接続できるアダプタが用意されている
- Sidekiq
- Resque
- Delayed Job
- etc...
今回は小規模なアプリケーションですのでRailsが提供するインプロセスのキューイングシステムを利用しても問題ないと考えましたが、学習のためにもSidekiqを利用することにしました。
###実装
####作成しているアプリケーションの概要
- 簡易的な会計ソフト
- ER図(一部抜粋)
- 仕訳のCSVインポート機能作成
- importsテーブルに仕訳を仮登録
- importsテーブルのレコードからjournalsテーブルにレコードを作成し残高更新
- importsテーブルのレコードを削除
- 500件のCSVデータで試したところ1.は正常に動き、2.の際に仕訳の二重登録、残高更新メソッドの不具合が発生しました
####開発環境
- Ruby 2.6.3
- Rails 5.2.6
- IDE:Cloud9
####本番環境
- AWS (EC2、RDS)
- Nginx、 Puma
####Active Jobを利用するための流れ
①ジョブを作成する
$ rails generate job csv_import
Running via Spring preloader in process 9813
invoke test_unit
create test/jobs/csv_import_job_test.rb
create app/jobs/csv_import_job.rb
②ジョブを実行できるか確認する
①で作成したcsv_import_job(以下jobと記載)にて、簡単な操作を記述します
class CsvImportJob < ApplicationJob
queue_as :default
def perform(*args)
p 'Hello World!'
end
end
トップ画面を表示した際に呼び出しがうまくいくかどうか確かめます。
ジョブクラスにperform_laterメソッドを適用すると、「キューイングシステムが空いたらジョブを実行する」とキューに登録することができます。
class HomesController < ApplicationController
def top
CsvImportJob.perform_later
end
end
ターミナルを確認すると意図した通りにjobを呼び出すことができていました。
③Sidekiqの設定
今回はアダプタとしてSidekiqを利用しますので、その設定を行います。Getting Startedの手順に従い設定していきます。また、Sidekiqを利用するにはRedisが必要ですので、先にRedisのインストールを行います。
1. Redisのインストール
- Redisをインストールするためにbrewのインストール(今回はcloud9にインストールする手順です)
LinuxbrewのREADME通りに下記コマンドを入力していきます
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"
$ test -d ~/.linuxbrew && eval $(~/.linuxbrew/bin/brew shellenv)
$ test -d /home/linuxbrew/.linuxbrew && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv)
$ test -r ~/.bash_profile && echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.bash_profile
$ echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile
$ brew install hello
参考
Linuxbrew
- Redisのインストール
$ brew install redis
- Redisの起動
$ redis-server
2. gemファイルのインストール
gem 'sidekiq'
gem 'redis'
$ bundle install
SidekiqはActive Jobを使わずとも、使用することができますので、Getting Startedにはworkerの設定が記述してあります。今回はActive Jobを利用しますので、以下Wiki/Active Jobを確認し進めます。
3. Sidekiqをアダプタとして利用する準備
class Application < Rails::Application
# ...
config.active_job.queue_adapter = :sidekiq
end
4. Sidekiqを起動してジョブを利用できるか確認する
bundle exec sidekiq
開発環境では、Redis用、Sidekiq用、Railsサーバ用の3つのターミナルを立てることになりますが、トップ画面をリロードすると
- Railsサーバ用のターミナルでは、ジョブの作成
- Sidekiq用のターミナルでは、ジョブの実行
を確認することができました。
Sidekiqの設定は以上で終了です。
④実装コードのリファクタリング
今回はimportsテーブルに仮登録した仕訳をjournalsテーブルに移す部分をActive Jobを用いて実装していきます。まずはリファクタリング前のコードは下記のようになっています。(#index,#importのみ抜粋)
def index
@imports = Import.where(user_id: current_user.id)
end
def import
imports = Import.where(user_id: current_user.id)
imports.each do |import|
journal = Journal.new
journal.user_id = import.user_id
journal.debit_id = import.debit_id
journal.credit_id = import.credit_id
journal.date = import.date
journal.amount = import.amount
journal.description = import.description
if journal.save
#application controllerに定義した残高を更新するためのメソッド
update_debit_and_credit_balance(journal.date.month, journal.debit_id, journal.credit_id, journal.amount)
import.destroy
else
flash[:danger] = '取込に失敗した仕訳があります。'
end
end
flash[:success] = '仕訳の取込を終了しました。'
redirect_to imports_path
end
imports.each部分をActive Jobに任せ、バックグラウンドで実行する実装に変更していきます。
また、現在#indexでは全ての仮登録データを表示しています。#import終了後imports_pathにリダイレクトした際、再度仮登録データが表示されていると、処理ができていないのかと誤解を招く可能性がありますので、importsテーブルにカラムを追加し待機中の状態を表現したいと思います。
- 変更後のER図
処理の流れ
- importsテーブルにて、現在のユーザ&待機中ではないレコードを全て取得
- 取得したレコードを待機中に変更し、その後の処理はバックグランドに任せる
- imports_pathにリダイレクト(#indexでは現在のユーザ&待機中ではないレコードを全て表示)
リファクタリング後の記述
def index
@imports = Import.where(user_id: current_user.id, pending: false)
end
def import
imports = Import.where(user_id: current_user.id, pending: false)
# 取得した仕訳を待機中に変更
imports.update_all(pending: true)
CsvImportJob.perform_later(current_user.id)
flash[:success] = '仕訳の取込を実行しています。しばらくしてから画面を再表示してください。'
redirect_to imports_path
end
class CsvImportJob < ApplicationJob
queue_as :default
sidekiq_options retry: false
def perform(user_id)
imports = Import.where(user_id: user_id, pending: true)
imports.each do |import|
journal = Journal.new
journal.user_id = import.user_id
journal.debit_id = import.debit_id
journal.credit_id = import.credit_id
journal.date = import.date
journal.amount = import.amount
journal.description = import.description
journal.save
# 貸借の残高を更新(application controllerに定義したメソッドを利用する方法が分からなかったので再定義)
debit_account = Account.find(journal.debit_id)
debit_account.update_balance(journal.amount, journal.date.month, 'debit')
credit_account = Account.find(journal.credit_id)
credit_account.update_balance(journal.amount, journal.date.month, 'credit')
import.destroy
end
end
end
ポイントは下記3点です。
- csv_import_job.rb #performではuser_idを引数に取っています。引数に取れる型は決まっているので、こちらで確認してください。
- sidekiq_options retry: false と記述することで処理に失敗した際にリトライしないようにしています。
- application controllerにて定義していたメソッドをActive Jobにて利用する方法が分かりませんでしたので、再度記述しています。
⑤本番環境設定
最後に本番環境で実行できるように設定を変更していきます。
本番環境にRedisインストール
AmazonLinux2では下記コマンドだけで出来てしまいます。
sudo amazon-linux-extras install redis4.0
Redisをデーモン化したいので、redis.conf内の記述を変更
$ sudo vi /etc/redis.conf
################################# GENERAL #####################################
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
redis.confに従ってRedis起動
$ sudo redis-server /etc/redis.conf
# 下記コマンドで動作しているかを調べる。PONGが帰って来れば動作している。
$ redis-cli ping
PONG
本番環境でSidekiqを起動
bundle exec sidekiq -e production
以上で終了です。本番環境で実行したところ、仕訳の二重登録や残高更新メソッドの不具合等はなく、無事に目的を達することができました。
Sidekiqのdaemon化について(2021/9/30追記)
Sidekiq v6以前は下記コマンドでdaemon化が出来ていたようですが、v6以降はオペレーティングシステムを利用してバックグランドで実行する必要があります。
bundle exec sidekiq -e production -d
実装方法はwiki/Deploymentに記述がありますので、これに従って実装していきます。
sidekiq.serviceの導入
sampleはこちらにあります。先程のwikiにもリンクがあります。
sampleのコメントに従って、/usr/lib/systemd/system に入れます。
sidekiq.serviceの設定
環境にだいぶ依存すると思うので、細かい記述は無しにします。コメントに従って作業を進めてください。
###おわりに
今回は仮登録のデータを本登録する流れでActive Jobを利用しましたが、CSVインポートで仮登録する段階や、その他機能の残高更新の際にも利用し処理待ち時間の短縮化を図りたいと思いました。
また、このままではインポートの進捗状況がユーザに伝わらないので、進捗状況をビュー側に表示する機能も作成していきたいと思います。