Sidekiq + Redis + ActiveJobでLINE定期通知機能を実装してみた

## はじめに

個人開発にて、`Sidekiq`、`Redis`、`ActiveJob`を組み合わせたLINE通知機能を実装しました。

備忘録として、実装の流れをまとめていきたいと思います！

**▼個人開発についてまとめた記事**

https://qiita.com/rk2530/items/61b1532dffef6382af77

:::note warn
私はプログラミング学習中で、初学者です。
内容に誤りがある場合がございます。
もし間違いがあればご指摘いただけますと幸いです。
:::

## 環境
- Ruby 3.2.2
- Ruby on Rails 7.0.8
- Render

## 事前準備
- [Render](https://render.com/)アカウントの作成
- [LINE Developersコンソール](https://developers.line.biz/ja/docs/line-developers-console/)で[チャネルを作成](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console)し、チャネルアクセストークンとチャネルシークレットを発行

## 手順
### （1）環境の準備
1. `Redis`のセットアップ
2. `Sidekiq`のインストール
3. `ActiveJob`の設定

### （2）LINE通知機能の実装
1. `LINE Messaging API`の設定
2. LINE通知サービスの作成

### （3）ActiveJobを使ったジョブの定義
ジョブクラスの作成

### （4）Sidekiq-Cronでの定期実行設定
1. `Sidekiq-Cron`の追加
2. 定期実行のスケジュール設定
3. `Sidekiq`の設定

### （5）動作確認、デプロイ
1. 動作確認
2. 手動での動作確認
3. デプロイ

長くなりますが、1つずつ書いていきます！

## （1）環境の準備
### 1. Redisのセットアップ
①Renderのダッシュボードにログインし、「New +」ボタンをクリックして、ドロップダウンメニューから「Redis」を選択します。

②[ドキュメント](https://docs.render.com/redis#setting-up-your-redis)に沿って、Redisサービスの名前・地域・プランなどを設定します。

③「Create Redis」ボタンをクリックして、Redisサービスが作成されたら、Renderのダッシュボードでそのサービスを開き、Connectionsの項目の「Internal Redis URL」を控えておきます。このアドレスは、他のRender内で動作するアプリケーションからRedisにアクセスするために使用します。

④「Internal Redis URL」を環境変数に設定して、アプリケーションからRedisに接続できるようにします。

```.env
REDIS_URL = XXXXXXXXXX
```
:::note info
**そもそもRedisとは？**
キーバリューストア型のインメモリデータベース（データをキーと値のペアとしてメモリ上に保存し、高速にアクセスできるよう設計されたデータベースシステムのこと）。高速なデータ書き込み・読み出しが可能で、データ構造の保存に対応している。Sidekiqでは、ジョブのキュー（待ち行列）、スケジュール、実行状態などを管理するためにRedisが使用される。
:::

### 2. Sidekiqのインストール
`Gemfile`にgem `gem 'sidekiq'`を追記して、`bundle install`を実行します。
```:Gemfile
gem 'sidekiq'
```
:::note info
**そもそもSidekiqとは？**
Rubyのバックグラウンド処理を行うためのライブラリ。マルチスレッド（コンピュータのプロセス内で複数のスレッド［実行の流れ］を同時に動作させる技術）を活用しており、Railsアプリケーションなどで非同期タスクを効率的に実行できる。Redisをジョブストア（バックグラウンドで実行するジョブの情報を一時的に保存しておく場所）として使用し、ジョブのキューイング（タスクやジョブを待ち行列［キュー］に入れ、先入れ先出しの原則に従って順番に処理すること）、実行、失敗時の再試行などを管理する。
:::

### 3. ActiveJobの設定
`Rails`で`ActiveJob`のバックエンドに`Sidekiq`を使用するよう設定します。
```config/environments/production.rb
  config.active_job.queue_adapter = :sidekiq
  # この設定により、ActiveJobがジョブをキューに入れる際にSidekiqを使用するようになる
  config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
  # RailsのキャッシュストアとしてRedisを使用する設定を追加
  # RedisサーバーのURLを環境変数から取得
```
:::note info
**そもそもActiveJobとは？**
ジョブを宣言し、それによってバックエンドでさまざまな方法によるキュー操作を実行するためのフレームワーク。
:::

:::note info
**キャッシュストア**
アプリケーションで使用される一時的なデータや計算結果を高速にアクセス可能な形で保存しておくためのストレージのこと。キャッシュストアは、データベースへのクエリ結果、計算結果、ウェブページのコンテンツ、APIのレスポンスなど、再利用可能な情報をメモリ内やディスク上に一時的に保存することで、アプリケーションのパフォーマンスを向上させる目的で使用される。
:::

※開発環境やテスト環境での設定を行いたい場合は、`config/environments/development.rb`や`config/environments/test.rb`に同様の設定を追加します。

## （2）LINE通知機能の実装
### 1. LINE Messaging APIの設定
チャネルアクセストークンとチャネルシークレットを発行する手順は、事前準備の欄に書いたため割愛させていただきます。

発行したチャネルアクセストークンとチャネルシークレットを環境変数に設定します。
```.env
LINE_CHANNEL_TOKEN = XXXXXXXXXX
LINE_CHANNEL_SECRET = XXXXXXXXXX
```

### 2. LINE通知サービスの作成
LINE通知を送信するサービスクラスを作成します。このクラスは、`LINE Messaging API`へのHTTPリクエストを行い、メッセージを送信します。

```app/services/line_notify_service.rb
require 'net/http'
require 'uri'
require 'json'

class LineNotifyService
  def self.send_message(uid, message)
    uri = URI.parse('https://api.line.me/v2/bot/message/push')
    # LINEのメッセージ送信APIのエンドポイントURLを解析してURIオブジェクトを生成
    request = Net::HTTP::Post.new(uri)
    # 上で解析したURIを使ってPOSTリクエストのオブジェクトを生成
    request.content_type = 'application/json'
    # リクエストのコンテンツタイプをJSONに設定
    request['Authorization'] = "Bearer #{ENV['LINE_CHANNEL_TOKEN']}"
    # 環境変数から取得したLINEチャネルトークンを使用して、認証ヘッダーを設定
    request.body = JSON.dump({
                               to: uid,
                               messages: [{ type: 'text', text: message }]
                             })
    # リクエストボディに、送信先のユーザー識別子と、送信するメッセージの内容をJSON形式で設定

    req_options = { use_ssl: uri.scheme == 'https', read_timeout: 10, open_timeout: 5 }
    # リクエストオプションを設定
    # HTTPSを使用し、読み取りタイムアウトとオープンタイムアウトの値を指定
    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(request)
    end
    # 上記で設定したオプションを用いてHTTPリクエストを送信し、レスポンスを受け取る

    handle_response(response)
  end

  def self.handle_response(response)
    case response
    when Net::HTTPSuccess
      Rails.logger.info 'LINE通知 送信成功'
    when Net::HTTPUnauthorized
      Rails.logger.error 'LINE通知 送信失敗: 認証エラー'
    when Net::HTTPNotFound
      Rails.logger.error 'LINE通知 送信失敗: リソースが見つかりません'
    else
      Rails.logger.error "LINE通知 送信失敗: #{response.code} #{response.message}"
      Rails.logger.error "レスポンスボディ: #{response.body}"
    end
  end
end
```
:::note info
**読み取りタイムアウト（Read Timeout）**
リクエストを送信した後、サーバーからのレスポンスデータを読み込み始めるまでの時間。この時間を超えると、クライアントはタイムアウトエラー（例：Net::ReadTimeout）を発生させ、処理を中断する。サーバーが過負荷で応答が遅い場合や、ネットワークの遅延が発生している場合に重要になる。

**オープンタイムアウト（Open Timeout）**
リクエストを開始してからサーバーへの接続が確立されるまでの最大待ち時間を指す（＝TCPコネクションが確立するまでの時間）。この時間内に接続が確立されない場合（例：サーバーがダウンしている、ネットワーク障害が発生しているなど）、クライアントはタイムアウトエラー（例：Net::OpenTimeout）を発生させる。

**TCPコネクション**
Transmission Control Protocol（TCP）を使用してネットワーク上の2つのデバイス（例：クライアントとサーバー）間で確立される信頼性の高い通信接続のこと。
:::

## （3）ActiveJobを使ったジョブの定義
ジョブクラスを作成します。例として、ユーザーに対して定期的なリマインダーを送信する場合のジョブを定義します。
```app/jobs/reminder_notification_job.rb
class ReminderNotificationJob < ApplicationJob
  queue_as :default

  def perform
    User.find_each do |user|
    # 全ユーザーを対象にリマインダー通知を送信する
      next unless user.receive_reminder_notifications
      # ユーザーが通知を受け取る設定になっているかチェック
      reminder_message = generate_reminder_message(user)
      # 通知メッセージをカスタマイズ
      LineNotifyService.send_message(user.uid, reminder_message) if user.uid.present?
      # LINE通知サービスを使用してリマインダーを送信
    end
  end

  private

  def generate_reminder_message(user)
    "こんにちは、#{user.name}さん。今日のタスクを忘れずにチェックしてくださいね！"
  end
end
```
## （4）Sidekiq-Cronでの定期実行設定
### 1. Sidekiq-Cronの追加
`Gemfile`に`gem 'sidekiq-cron'`を追記して、`bundle install`を実行します。
```:Gemfile
gem 'sidekiq-cron'
```

### 2. 定期実行のスケジュール設定
`config/schedule.yml`などの設定ファイルを作成し、定期的に実行したいジョブとそのスケジュールを定義します。
```config/schedule.yml
reminder_notification_job:
  cron: "0 10 * * *"
  class: "ReminderNotificationJob"
```
:::note info
スケジュールの指定方法は、左から順に
1. **分**（0から59）
2. **時**（0から23）
3. **日**（1から31）
4. **月**（1から12）
5. **曜日**（0（日曜日）から6（土曜日））

アスタリスクは「任意の値」という意味で使われ、具体的な数値を指定しない場合に使用
:::

### 3. Sidekiqの設定
`Sidekiq`の初期化ファイル（例：`config/initializers/sidekiq.rb`）に`Sidekiq-Cron`のジョブを読み込む設定を追加します。
```config/initializers/sidekiq.rb
require 'sidekiq'
require 'sidekiq-cron'

Sidekiq.configure_server do |config|
# Sidekiqサーバーの設定を開始 ブロック内の設定は、ジョブを実行するサーバー側で適用
  schedule_file = 'config/schedule.yml'
  # 定期的に実行するジョブのスケジュールを記述したYAMLファイルのパスを指定
  config.redis = {
    url: ENV['REDIS_URL'],
    # Redisへの接続設定 ENV['REDIS_URL']は環境変数からRedisサーバーのURLを取得
    connect_timeout: 5,
    read_timeout: 5,
    write_timeout: 5
    # 接続、読み取り、書き込みそれぞれに5秒のタイムアウトを設定
  }

  Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) if File.exist?(schedule_file)
  # config/schedule.ymlファイルが存在する場合、そのファイルを読み込み、
  # YAMLファイルに記述されたスケジュールに従って定期的なジョブを設定
end

Sidekiq.configure_client do |config|
# Sidekiqクライアントの設定を開始　この設定は、ジョブをキューに投入する側で適用
  config.redis = {
    url: ENV['REDIS_URL'],
    # Redisへの接続設定 ENV['REDIS_URL']は環境変数からRedisサーバーのURLを取得
    connect_timeout: 5,
    read_timeout: 5,
    write_timeout: 5
    # 接続、読み込み、書き込みそれぞれに5秒のタイムアウトを設定
  }
end
```
## （5）動作確認、デプロイ
### 1. Sidekiqのダッシュボードでの動作確認
定期的にLINE通知が送られるかどうかを確認します。

`Sidekiq`のダッシュボードを使ってジョブが正しくエンキューされているかを確認できます。

①ルーティングの設定
```config/routes.rb
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
```
②`Sidekiq`のダッシュボードでジョブの状況を確認
`/sidekiq`でアクセスした際に`Sidekiq`のダッシュボードが表示され、エンキューされているジョブの状況を確認できます。

[![Image from Gyazo](https://i.gyazo.com/f5b9932673f009c4309d52280c929e27.png)](https://gyazo.com/f5b9932673f009c4309d52280c929e27)

### 2. 手動での動作確認
`Rails`コンソールを使用して、手動でジョブの動作を確認することもできます。

コンソールを起動して、実行したいジョブのクラス名と`perform_later`メソッドや`perform_now`メソッドを使ってジョブを実行します。

- `perform_later`メソッド　ジョブを非同期で実行する（ジョブシステムのキューにジョブをエンキューする）場合に使用
- `perform_now`メソッド　すぐにジョブを実行したい場合に使用

```:例
ReminderNotificationJob.perform_now
```
### 3. デプロイ
動作確認ができたら、各ホスティングサービスの設定方法に則り、`Redis`サーバーへの接続情報（例：`REDIS_URL`）などを環境変数に設定し、アプリケーションを本番環境にデプロイします。

:::note info
**エンキュー（Enqueue）**
キューに新しい要素を追加する操作。キューの末尾（最後）に要素が追加され、キュー内の要素数が一つ増える。バックグラウンドジョブ処理システムにおいて、新しいジョブをキューに追加する行為をエンキューと呼ぶ。これにより、ジョブが後で処理されるための「待ち行列」に入れられる。

**デキュー（Dequeue）**
キューから要素を取り出す操作。キューの先頭（最初）にある要素が取り出され、キューからその要素が削除される。バックグラウンドジョブ処理システムにおいて、処理が準備できたジョブをキューから取り出して実行することをデキューと呼ぶ。ジョブがデキューされると、そのジョブはキューから削除され、即座にまたはスケジュールに従って処理が開始される。
:::

## おわりに
長くなってしまいましたが、文字に起こしてみることで、より理解を深めることができたかなと思います。
まだまだ勉強不足なので、引き続き学習に励んでいきたいと思います！

最後まで読んでいただきありがとうございました。