Active Jobについて

2021/9/30追記 sidekiqのdaemon化について

###はじめに
Railsでアプリケーションを開発中、500件のレコードを作成し、各レコードに対してメソッドを実行していたところ、レコードが二重で作成されたり、メソッドがうまく実行されていないなど予期せぬ挙動を起こしました。原因を完全に突き止めることは出来ませんでしたが、おそらく処理が重たくなり、リクエストが二重で発生してしまったのではないかと考えています。
そこで、一連の処理をActive Jobに任せ、バックグラウンドで実行する実装に変更しました。
キューやジョブなど概念として知っていた内容を初めて実装に紐づけることができましたので、合わせてまとめたいと思います。

###Active Jobについて
####概要
Active JobとはRails標準のジョブを処理するフレームワークです。このフレームワークを利用することで、キュー操作をバックエンドで実行することができるようになります。キューイングシステムが空いたらジョブを実行する、指定した時間になったらジョブを実行するなどの指定も可能です。

####キューとジョブとは
簡単に説明するとキューという枠にジョブという単位の箱を入れていき、先に入れたものから先に取り出され実行されるというものです。
<img width='200' alt='que_and_job' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1585805/9e92ad43-6338-0c5d-8b56-dca322a6e193.png'>

####キューイングシステム
- インプロセスのキューイングシステム
    - ジョブをメモリに保持する
    - プロセスがクラッシュしたりコンピュータをリセットするとジョブが失われる
    - Rails標準搭載
- アウトオブプロセスのキューイングシステム
    - 別のプロセスを立ててキューを管理する
    - Active Jobには様々なキューイングバックエンドに接続できるアダプタが用意されている
        - Sidekiq
        - Resque
        - Delayed Job
        - etc...

今回は小規模なアプリケーションですのでRailsが提供するインプロセスのキューイングシステムを利用しても問題ないと考えましたが、学習のためにもSidekiqを利用することにしました。

>参考記事
>[Active Job の基礎 Railsガイド](https://railsguides.jp/active_job_basics.html)

###実装
####作成しているアプリケーションの概要
- 簡易的な会計ソフト
- ER図（一部抜粋）
<img width='500' alt='erd' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1585805/d61849bc-17b4-445d-0d62-9d2a8c3868e0.png'>

- 仕訳のCSVインポート機能作成
 1. importsテーブルに仕訳を仮登録
 1. importsテーブルのレコードからjournalsテーブルにレコードを作成し残高更新
 1. 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と記載）にて、簡単な操作を記述します

```ruby:app/jobs/csv_import_job.rb
class CsvImportJob < ApplicationJob
  queue_as :default

  def perform(*args)
    p 'Hello World!'
  end
end
```

トップ画面を表示した際に呼び出しがうまくいくかどうか確かめます。
ジョブクラスにperform_laterメソッドを適用すると、「キューイングシステムが空いたらジョブを実行する」とキューに登録することができます。

```ruby:app/controllers/homes_controller.rb
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](https://github.com/Linuxbrew/brew)

- Redisのインストール

```
$ brew install redis
```

- Redisの起動

```
$ redis-server
```

###### 2. gemファイルのインストール

```ruby:Gemfile
gem 'sidekiq'
gem 'redis'
```

```
$ bundle install
```

SidekiqはActive Jobを使わずとも、使用することができますので、Getting Startedにはworkerの設定が記述してあります。今回はActive Jobを利用しますので、以下Wiki/Active Jobを確認し進めます。

>参考
>[Sidekiq/Wiki/Getting Started](https://github.com/mperham/sidekiq/wiki/Getting-Started)
>[Sidekiq/Wiki/Active Job](https://github.com/mperham/sidekiq/wiki/Active-Job)

###### 3. Sidekiqをアダプタとして利用する準備

```ruby:config/application.rb
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のみ抜粋）

```ruby:imports_controller.rb
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図
<img width='200' alt='erd' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1585805/0c1e1261-f9e8-7b76-4750-954d7b707d48.png'>

###### 処理の流れ
1. importsテーブルにて、現在のユーザ&待機中ではないレコードを全て取得
1. 取得したレコードを待機中に変更し、その後の処理はバックグランドに任せる
1. imports_pathにリダイレクト（#indexでは現在のユーザ&待機中ではないレコードを全て表示）

###### リファクタリング後の記述

```ruby:imports_controller.rb
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
```

```ruby:app/jobs/csv_import_job.rb
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点です。
1. csv_import_job.rb #performではuser_idを引数に取っています。引数に取れる型は決まっているので、[こちら](https://railsguides.jp/active_job_basics.html#%E5%BC%95%E6%95%B0%E3%81%A7%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%81%95%E3%82%8C%E3%82%8B%E5%9E%8B)で確認してください。
2. sidekiq_options retry: false　と記述することで処理に失敗した際にリトライしないようにしています。
3. application controllerにて定義していたメソッドをActive Jobにて利用する方法が分かりませんでしたので、再度記述しています。

##### ⑤本番環境設定
最後に本番環境で実行できるように設定を変更していきます。
###### 本番環境にRedisインストール
AmazonLinux2では下記コマンドだけで出来てしまいます。

```
sudo amazon-linux-extras install redis4.0
```
###### Redisをデーモン化したいので、redis.conf内の記述を変更

```
 $ sudo vi /etc/redis.conf
```

```ruby:/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起動
```ruby
$ sudo redis-server /etc/redis.conf
# 下記コマンドで動作しているかを調べる。PONGが帰って来れば動作している。
$ redis-cli ping
PONG
```
>参考
>[Redis クイック スタート](http://mogile.web.fc2.com/redis/topics/quickstart.html)

###### 本番環境でSidekiqを起動
```
bundle exec sidekiq -e production
```

以上で終了です。本番環境で実行したところ、仕訳の二重登録や残高更新メソッドの不具合等はなく、無事に目的を達することができました。

##### Sidekiqのdaemon化について(2021/9/30追記)
Sidekiq v6以前は下記コマンドでdaemon化が出来ていたようですが、v6以降はオペレーティングシステムを利用してバックグランドで実行する必要があります。

```
bundle exec sidekiq -e production -d
```

>参考
>[Welcome to Sidekiq 6.0!](https://github.com/mperham/sidekiq/blob/master/6.0-Upgrade.md)

実装方法は[wiki/Deployment](https://github.com/mperham/sidekiq/wiki/Deployment)に記述がありますので、これに従って実装していきます。

###### sidekiq.serviceの導入
sampleは[こちら](https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service)にあります。先程のwikiにもリンクがあります。
sampleのコメントに従って、/usr/lib/systemd/system に入れます。

###### sidekiq.serviceの設定
環境にだいぶ依存すると思うので、細かい記述は無しにします。コメントに従って作業を進めてください。

###おわりに
今回は仮登録のデータを本登録する流れでActive Jobを利用しましたが、CSVインポートで仮登録する段階や、その他機能の残高更新の際にも利用し処理待ち時間の短縮化を図りたいと思いました。
また、このままではインポートの進捗状況がユーザに伝わらないので、進捗状況をビュー側に表示する機能も作成していきたいと思います。