😊

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

2022/07/24に公開

自分のスケジュールを家族のLINEに通知してくれるRailsアプリを個人開発しています。その中で機能的に必要だった非同期処理を、Redis+Sidekiq+Active Jobという構成で実装したのでそのメモです。

https://torikomi.fly.dev/

非同期処理とは

「Railsサーバーとは別のサーバーをたてて、バックグラウンドでジョブを実行すること」です。

通常のRailsの処理は、

  1. クライアントからHTTPリクエストがRailsサーバーに送られる
  2. Railsがルーティングに従ってコントローラーとアクションを特定し、データベースに適切な処理を行う
  3. RailsがビューをまとめてHTTPレスポンスとしてクライアントに返す

というものです。

この一連の流れとは別に走らせたい処理、例えば2.のコントローラ内で実行するには時間がかかりすぎる重たい処理や、定期的なクリーンアップ、メール送信などの処理は、Railsサーバーとは別のサーバー上で非同期処理で行うと効率がよいです。

Railsで非同期処理を実行するフレームワークがActive Job

じゃあRailsでどうやって非同期処理を実装するのか、というところなのですが、Railsガイドを眺めていると、「Active Job」というフレームワークを発見。

https://railsguides.jp/active_job_basics.html

「ジョブを宣言し、それによってバックエンドでさまざまな方法によるキュー操作を実行するためのフレームワーク」とあります。

ただ注意したいのは、Active Jobだけで非同期処理が完結するわけでなく、Active Jobでキューイングされたジョブを実行する環境が別に必要となる点。調査するとRedis+Sidekiqという構成が多く使われていたので、こちらもあわせて使うことにしました。

https://zenn.dev/shima_zu/articles/rails_active_job

https://dev.icare.jpn.com/dev_cat/sidekiq/

言うなれば、Active Jobは洗濯物をホイホイ洗濯かごに投げ込んでいるだけの人で、洗濯かごを管理する人(Redis)、洗濯かごから洗濯物をとって洗濯機を回す人(Sidekiq)も必要ということです。

後々、Sidekiqに洗濯物を洗濯かごに投げ込む機能もあることがわかったので、「あれっ、Active Jobいらなくね?」とはなったのですが、Railsデフォルトのフレームワークなので一応使ってみます。

バージョン情報

以下の環境で作成しています。

  • macOS 11.6.2
  • Ruby 3.1.2
  • Rails 7.0.3
  • PostgreSQL 14.4
  • redis 7.0.3
  • sidekiq 6.5.1

下準備

今回は、「Railsアプリに登録されているスケジュールを、その開始日時にLINEメッセージで通知する」というジョブの実装を考えます。なお、LINE通知の処理の部分までコードを書くと長くなってしまうので、ここではLINEではなくコンソールで表示させる簡易版にします。

まず、scaffoldコマンドでスケジュールのMVCを追加します。

Railsアプリのワーキングディレクトリに移動して下記のコマンドを実行。

$ bundle exec rails g scaffold Schedule title:string start_time:timestamp end_time:timestamp

のちのちジョブを実行する際にエラーが起きないように、すべてのカラムにnull制約をつけてからrails db:migrateします。

db/migrate/XXXX_create_schedules.rb
class CreateSchedules < ActiveRecord::Migration[7.0]
  def change
    create_table :schedules do |t|
      t.string :title, null: false
      t.timestamp :start_time, null: false
      t.timestamp :end_time, null: false

      t.timestamps
    end
  end
end
$ bundle exec rails db:migrate

モデルにもバリデーションを追加します。

app/models/schedule.rb
class Schedule < ApplicationRecord
  # 以下の3行を追加
  validates :title, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true
end

Redisのインストール

次にRedisをインストールします。RedisはSidekiqと一緒に使うデータベースです。

$ brew install redis

Redisサーバーの起動コマンドはredis-serverですが、これをそのまま実行するとカレントディレクトリにダンプファイル(データベースの本体)が保存されてしまいます。

https://blog.kotamiyake.me/tech/output-dump-rdb-to-current-directory/

設定ファイルのパスを指定して起動すると、正しい場所にダンプファイルが保存されるようになります。

$ redis-server /usr/local/etc/redis.conf

コンソールにこんな画面が流れてきたらredisの起動完了です!

59959:C 23 Jul 2022 23:32:55.416 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo              
59959:C 23 Jul 2022 23:32:55.416 # Redis version=7.0.3, bits=64, commit=00000000, modified=0, pid=59959, just started
59959:C 23 Jul 2022 23:32:55.416 # Configuration loaded                                       
59959:M 23 Jul 2022 23:32:55.417 * Increased maximum number of open files to 10032 (it was originally set to 256).
59959:M 23 Jul 2022 23:32:55.417 * monotonic clock: POSIX clock_gettime                       
                _._                                                                           
           _.-``__ ''-._                                                                      
      _.-``    `.  `_.  ''-._           Redis 7.0.3 (00000000/0) 64 bit                       
  .-`` .-```.  ```\/    _.,_ ''-._                                                            
 (    '      ,       .-`  | `,    )     Running in standalone mode                            
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379                                            
 |    `-._   `._    /     _.-'    |     PID: 59959                                            
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'  

Sidekiqのインストール

次にバックグラウンドの実行プロセスを担当するSidekiqをインストールします。

https://github.com/mperham/sidekiq

Sidekiqはgem経由でインストールできます。Gemfileにgem sidekiqを追加して、コマンドでbundle installを実行。

Gemfile
gem 'sidekiq'
$ bundle install

/config/initializers/配下にsidekiq.rbファイルを新規作成し、SidekiqのデータベースとしてRedisを使うことを宣言します。

/config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://127.0.0.1:6379' }
end

Sidekiq.configure_client do |config|
  config.redis = { url: 'redis://127.0.0.1:6379' }
end

また/config/配下にsidekiq.ymlを作成すれば、起動時のオプションを設定できます。sidekiqのGitHubにあるexampleを見るといろいろ設定できるようなのですが、何をすべきなのかよくわからない……。

https://qiita.com/ryohashimoto/items/69dac29a63f682143df7

logfileでログの出力先だけ設定しようと思ったのですが、これもSidekiq6から廃止されてるんですね。このあたりは勉強してから挑むことにして、今回は何も設定しませんでした。

最後に、config/application.rbでActive JobのバックエンドにSidekiqを使用することを宣言します。ついでにアプリのタイムゾーンを東京にしておきます。

config/application.rb
module YourApp
  class Application < Rails::Application

    # 下記を追加
    config.active_job.queue_adapter = :sidekiq
    
    # タイムゾーンの設定をしていない場合は下記2行を追加
    config.active_record.default_timezone = :local
    config.time_zone = 'Tokyo'
  end
end

下記のコマンドを実行してSidekiqを起動。

$ bundle exec sidekiq

こんな感じの画面が出てきたら起動成功です! なんかキックしてる人いる。

               m,
               `$b
          .ss,  $$:         .,d$
          `$$P,d$P'    .,md$P"'
           ,$$$$$b/md$$$P^'
         .d$$$$$$/$$$P'
         $$^' `"/$$$'       ____  _     _      _    _
         $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
         `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                $$:         ___) | | (_| |  __/   <| | (_| |
                $$         |____/|_|\__,_|\___|_|\_\_|\__, |
              .d$$                                       |_|

Active Jobを作成する

準備が整ったので、Active JobのジョブをRedis+Sidekiq環境下で動かしてみましょう。下記コマンドでActive Jobのジョブを生成します。

$ bundle exec rails g job send_notification

app/jobs/配下にジョブファイルが作成されます。peformメソッドの中に非同期処理で実行したい内容を記述します。また、引数をとることもできます(引数は配列で渡されることに注意!)。

app/jobs/send_notification_job.rb
class SendNotificationJob < ApplicationJob
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

先に作成したSchedulesコントローラーで、新しくScheduleが登録されたときに、その開始日時(start_time)にジョブが実行されるように設定します。

ジョブの実行日時を指定する方法は、setオプションを用いて、[ジョブのクラス名].set(wait_until: [ジョブの実行日時]).perform_laterとなります。

また、ジョブ側で該当するScheduleをとってこれるように、引数でScheduleのidを渡します。

app/controllers/schedules_controller.rb
class SchedulesController < ApplicationController

  # 略

  # POST /schedules or /schedules.json
  def create
    @schedule = Schedule.new(schedule_params)

    respond_to do |format|
      if @schedule.save
	# 下記1行を追加
	SendNotificationJob.set(wait_until: @schedule.start_time).perform_later(@schedule.id)
        
	format.html { redirect_to schedule_url(@schedule), notice: "Schedule was successfully created." }
        format.json { render :show, status: :created, location: @schedule }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @schedule.errors, status: :unprocessable_entity }
      end
    end
  end

  # 略

end

app/jobs/send_notification_job.rbにジョブの処理を記述します。渡されたScheduleのidをもとに該当するスケジュールをとってきて、コンソールにそのタイトルを表示させます。

app/jobs/send_notification_job.rb
class SendNotificationJob < ApplicationJob
  queue_as :default

  def perform(*args)
    #下記3行を追加
    schedule = Schedule.find(args[0])
    puts 'スケジュールをお知らせします!'
    puts 'タイトル:' + schedule.title
  end
end

Redis、Sidekiqを起動させた状態で、Railsサーバーを起動。ブラウザから先の日時で適当なスケジュールを登録します。


rails newする際にtailwindCSSを入れたのでScaffoldで作成した画面にデザインがあたっています

スケジュールの開始日時になると、Sidekiqの実行画面にスケジュールのタイトルが表示されました! 成功です!

2022-07-24T10:07:01.160Z pid=62329 tid=14p1 class=SendNotificationJob jid=f55c6090cf279f4fe2ec43e4 INFO: start
2022-07-24T10:07:01.460Z pid=62329 tid=14p1 class=SendNotificationJob jid=f55c6090cf279f4fe2ec43e4 INFO: Performing SendNotificationJob (Job ID: 417f1926-c2ac-4862-a783-e4534a27159b) from Sidekiq(default) enqueued at 2022-07-24T10:05:43Z with arguments: 2
スケジュールをお知らせします!
タイトル:テスト スケジュール
2022-07-24T10:07:01.505Z pid=62329 tid=14p1 class=SendNotificationJob jid=f55c6090cf279f4fe2ec43e4 INFO: Performed SendNotificationJob (Job ID: 417f1926-c2ac-4862-a783-e4534a27159b) from Sidekiq(default) in 45.75ms
2022-07-24T10:07:01.506Z pid=62329 tid=14p1 class=SendNotificationJob jid=f55c6090cf279f4fe2ec43e4 elapsed=0.345 INFO: done

感想

というわけで、Redis+Sidekiq+Active Jobの構成で非同期処理を実装できました。サーバーやデータベースをいくつも起動してややこしく見えますが、やってみれば思ったより簡単です。

しかし検証したのはまだdevelopment環境だけです。production環境(デプロイ先はherokuを想定しています)でもスムーズにいけるのか……がんばりたいです。

https://madogiwa0124.hatenablog.com/entry/2021/03/28/161255

今回はジョブを登録するところまでしか説明していませんが、ジョブを削除したい場面も出てくるかと思います。これは、sidekiq/apiを利用することで実装できたので、これについても今後の記事でまとめていこうかなと思います。

https://github.com/mperham/sidekiq/wiki/API

Discussion