【Rails7】Redis+Sidekiq+Active Jobで指定した日時に非同期処理を行う
自分のスケジュールを家族のLINEに通知してくれるRailsアプリを個人開発しています。その中で機能的に必要だった非同期処理を、Redis+Sidekiq+Active Job
という構成で実装したのでそのメモです。
非同期処理とは
「Railsサーバーとは別のサーバーをたてて、バックグラウンドでジョブを実行すること」です。
通常のRailsの処理は、
- クライアントからHTTPリクエストがRailsサーバーに送られる
- Railsがルーティングに従ってコントローラーとアクションを特定し、データベースに適切な処理を行う
- RailsがビューをまとめてHTTPレスポンスとしてクライアントに返す
というものです。
この一連の流れとは別に走らせたい処理、例えば2.のコントローラ内で実行するには時間がかかりすぎる重たい処理や、定期的なクリーンアップ、メール送信などの処理は、Railsサーバーとは別のサーバー上で非同期処理で行うと効率がよいです。
Railsで非同期処理を実行するフレームワークがActive Job
じゃあRailsでどうやって非同期処理を実装するのか、というところなのですが、Railsガイドを眺めていると、「Active Job」というフレームワークを発見。
「ジョブを宣言し、それによってバックエンドでさまざまな方法によるキュー操作を実行するためのフレームワーク」とあります。
ただ注意したいのは、Active Jobだけで非同期処理が完結するわけでなく、Active Jobでキューイングされたジョブを実行する環境が別に必要となる点。調査するとRedis+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
します。
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
モデルにもバリデーションを追加します。
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
ですが、これをそのまま実行するとカレントディレクトリにダンプファイル(データベースの本体)が保存されてしまいます。
設定ファイルのパスを指定して起動すると、正しい場所にダンプファイルが保存されるようになります。
$ 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をインストールします。
Sidekiqはgem経由でインストールできます。Gemfileにgem sidekiq
を追加して、コマンドでbundle install
を実行。
gem 'sidekiq'
$ bundle install
/config/initializers/
配下にsidekiq.rb
ファイルを新規作成し、SidekiqのデータベースとしてRedisを使うことを宣言します。
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を見るといろいろ設定できるようなのですが、何をすべきなのかよくわからない……。
logfile
でログの出力先だけ設定しようと思ったのですが、これもSidekiq6から廃止されてるんですね。このあたりは勉強してから挑むことにして、今回は何も設定しませんでした。
最後に、config/application.rb
でActive JobのバックエンドにSidekiqを使用することを宣言します。ついでにアプリのタイムゾーンを東京にしておきます。
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
メソッドの中に非同期処理で実行したい内容を記述します。また、引数をとることもできます(引数は配列で渡されることに注意!)。
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を渡します。
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をもとに該当するスケジュールをとってきて、コンソールにそのタイトルを表示させます。
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を想定しています)でもスムーズにいけるのか……がんばりたいです。
今回はジョブを登録するところまでしか説明していませんが、ジョブを削除したい場面も出てくるかと思います。これは、sidekiq/api
を利用することで実装できたので、これについても今後の記事でまとめていこうかなと思います。
Discussion