行動すれば次の現実

テック中心の個人ブログ

Asset Pipelineの本番モードと開発モードの挙動の違い

アセットパイプラインは本番環境と開発環境で挙動が違うと言われていますが、具体的にどのように違うのかを深堀りしてみました。

そもそも本番モード、開発モードとは?

アセットパイプラインでは本番モード、開発モードで挙動が違うというのはRails開発者なら誰しもが知っていることかと思います。しかし厳密には本番モード、開発モードという個々のモードが存在するわけではありません。Railsガイドの説明上、明示的に別の表現が用いられているにすぎません。

本番モードとはconfig.assets.compile=falseの状態であることを指します。

config.assets.compileとは動的コンパイルをするかどうか制御するオプションです。 trueの場合は動的コンパイルをする、falseの場合は動的コンパイルをしません。 本番環境の場合はデフォルトでfalseが設定されます。

動的コンパイルとは、リクエストを受けたときに、最新のアセットがコンパイルされていなかった場合に、その場でコンパイルを実行してアセットを生成する仕組みのことです。アセットのコンパイルには多くのメモリを消費するため、本番環境ではconfig.assets.compile=falseにすることが原則とされています。

反対に開発環境では、動的コンパイルを有効にすることで多少メモリを消費したとしても開発作業の効率を優先して、動的コンパイルを有効にするケースが一般的です。 開発環境の場合はデフォルトでtrueが設定されます。

本番モードの仕組み

本番モードの仕組みを簡単に説明します。本番モードでは以下のようなアセットパイプラインの設定になっているのが一般的です。

config.assets.compile=false
config.assets.digest=true

config.assets.digest=trueに設定することで、コンパイルされたのファイル名に以下のようなフィンガープリントが付与されます。

/assets/application-47140618c97a4b155143c9a405b56b34e8ed3c3aeb621fb1110dc1f41c99c34c.js

javascriptの場合、アセットパイプラインはjavascript_include_tagでリンクさせます。

<%= javascript_include_tag "application" %>

このように記載することで、出力されたHTMLには以下のようなアセットがリンクされます。

ここで出力された/assets/というディレクトリの実態はデフォルトでは、pubic/aseets配下になります。

本番モードでは、アセットコンパイルは手動で行う必要がありので、以下のコマンドを実行します。

RAILS_ENV=production bin/rails assets:precompile

コマンドを実行すると、pubic/aseetsにapplication.jsのフィンガープリント付きのアセットが生成されることが確認できます。

このフィンガープリントはアセットパイプラインがコンパイルする対象のディレクトリにある全てのファイルを連結させたファイルに対してSHA-256でハッシュ化された文字列が付与されます。

アセットの内容が少しでも変更されるとフィンガープリントも変更されます。これによりWebサーバーやCDN等のキャッシュ破棄が正常に動作するようになります。

開発モードの仕組み

次に開発モードの仕組みを説明します。開発モードでは以下の設定になっているのが一般的です。

config.assets.compile=true
config.assets.digest=true

本番モードとの違いはconfig.assets.compile(動的コンパイル)がtrueであるかどうかです。

開発モードの場合、assets:precompileのような明示的なコンパイルを実行しません。動的コンパイルが働きますので、あまり意識せずともアセットパイプラインがよしなに動いてくれます。どのようにコンパイルが実行されているかは良い意味でブラックボックス化されています。

詳細は省きますが、簡単に動的コンパイルの流れを説明すると以下のようになります。(Railsのバージョンによって多少の違いがありますのでご容赦ください)

  1. /assets/**へのリクエストをSprocketsが受け取る
  2. config.assets.compile=trueなので、初回リクエストの場合はアセットをコンパイルしてキャッシュに保存して返却する
  3. 以降のリクエストではアセットに変更がないことを確認する
  4. 変更がなければキャッシュされた内容を返却する
  5. 変更がある場合は再度コンパイルした結果をキャッシュに保存して返却する

ここでいうキャッシュとはSprocketsキャッシュストアを指します。デフォルトでは、tmp/cache/assetsのパスが設定されています。

開発モードの場合、アセットが最新であるかどうかの確認やコンパイルが実行されるため、本番モードと比べてメモリ消費が多く、パフォーマンスも低いという特徴があります。

また、Rails6.0の場合下記に該当するassetsはプロジェクトディレクトリのどこにも存在しません。

このことから、動的コンパイルモードの場合はアセットそのモノを作成せずに、キャッシュを使って動的にアセットを生成していることが伺えます。コンパイル対象となるアセットに変更がある場合は、キャッシュの中身を更新した後に、動的にアセットを生成しているようです。

参考

アセットパイプライン - Railsガイド

config.assets.compile=true in Rails production, why not? - Stack Overflow

inputファイル経由ではなく、アプリ内で作成したファイルをCarrierWaveで管理する方法

CarrierWaveの使用例として、inputファイルをアップロードしてModelとリンクさせるというケースが多いかと思います。

本記事では、inputファイルではなくRailsアプリケーションで作成したファイルをModelとリンクさせる方法を紹介します。

実装例

「Reportモデルのoutput_fileという項目にCarrierWaveをマウントしている」という体での実装イメージです。

# reportオブジェクトを取得します
report = Report.find(params[:id)

# tmp/hello.txtにファイルを作成します
file_path = 'tmp/hello.txt'
File.open(file_path, 'wb') do |file|
  file.puts('hello wolrd!')
end

# reportオブジェクトのoutput_fileにファイルをリンクさせます
File.open(file_path) do |file|
  report.output_file = file
end

# ファイルを削除します
FileUtils.rm_rf file_path

CarrierWaveを使用することで、Modelとファイルのライフサイクルを一体化することが出来ますので、とても保守が楽になります。

使用できる箇所では積極的に使用していくと良いでしょう。

参考

https://github.com/carrierwaveuploader/carrierwave#activerecord

FTPに配置されたファイルを取り込む処理をRSpecでテストする方法

外部サービスとデータ連携する際に、FTPに配置されたファイルをローカルにダウンロードして、データを取り込むような処理を実装するケースは少なくないと思います。

当該処理のユニットテストを実装する場合、毎回リアルなFTPサーバーを使ってテストするのは現実的ではありません。 本記事では、FTP通信の実装例とモックを使用したRSpecのテストコードを紹介したいと思います。

net/ftpをラップしたFtpClientクラスを用意

net/ftpはRuby標準のFTPライブラリです。そのまま使うには自由度が高すぎるので、専用のラッパークラスを介してnet/ftpを扱うようにします。

下記クラスはあくまでもサンプルなので作りが甘い部分があります。実際にはエラーハンドリングなどを考慮する必要があります。

lib/utils/ftp_client.rb

require 'net/ftp'

class FtpClient

  def initialize(host, user, password)
    @host = host
    @user = user
    @password = password
  end

  def connect
    @ftp = Net::FTP.new
    @ftp.connect(@host)
    @ftp.login(@user, @password)
  end

  def close
    @ftp.close
  end

  def copy_to_local(remote_filepath, local_filepath)
    @ftp.get(remote_filepath, local_filepath)
  end
end

FtpClientの使用例

FtpClientの使用例は以下の通りです。 ファイル取込用のサービスクラス(ImportDataSerice)で、FtpClientを使用する例を記します。

app/services/import_data_service.rb

class ImportDataSerice

  def call
    # ログイン
    # 実際にはログイン情報は環境変数などで渡したほうが良いです
    ftp_client = FtpClient.new('111.111.111.111', 'ftp-user', 'password')
    # 接続開始
    ftp_client.connect
    # ファイルダウンロード
    ftp_client.copy_to_local('ftp_dir/data.csv', 'tmp/data.csv')
    # 接続終了
    ftp_client.close

    # 以下に取込処理を実装する
  end
end

RSpecの実装例

ImportDataSericeをRSpecでテストしていきます。

FtpClientを実際に接続することを避けるためにFtpClientの各メソッドをallow_any_instance_ofでモック化します。 *1

connectとcloseメソッドは単純にtrueを返す実装に変更させます。

copy_to_localメソッドは、spec/data/services/import_data_service/data.csvにあるテストデータをtmp/data.csvにコピーするという処理に置き換えます。

こうすることで、ImportDataSericeの取込処理では、tmp/data.csvに配置された状態で動作することになります。

テストデータはFTPにアップロードすることなく、ローカルのspec/data/services/import_data_service/data.csvを直接編集すれば良いので楽にテストすることが出来ます。

require 'rails_helper'

RSpec.describe ImportDataService, type: :service do
  describe 'call' do
    example 'ファイルが取り込まれること' do
        allow_any_instance_of(FtpClient).to receive(:connect) do
          true
        end
        allow_any_instance_of(FtpClient).to receive(:close) do
          true
        end
        allow_any_instance_of(FtpClient).to receive(:copy_to_local) do
          FileUtils.cp('spec/data/services/import_data_service/data.csv', 'tmp/data.csv')
        end

        ImportDataService.new.call
        # 以下で取込結果を検証していく
    end
  end
end

*1:※ allow_any_instance_ofを使用していますが、別のメソッドでもモック化出来るかもしれません

新規システムをスムーズに開発する方法

今まで様々なシステムを新規開発してきましたが、毎回考えることは「スムーズに新規開発するにはどうしたら良いのか」ということです。

「前回よりも今回」という目標を持って、この問いに取り組んできましたが、ある程度答えが明確になってきたので、今回アウトプットすることにしました。

新規開発に苦戦している方の一助になれば幸いです。

まずは顧客の「要望」を「要件」に落とし込む

まずは顧客の頭の中にある要望を吸い上げて、要件に落とし込む作業を行います。

「顧客がどのような目的でシステムを作りたいのか」を一人称視点で理解する必要があります。

前提として、顧客と最低限会話できるレベルの業務知識を持っておく必要があります。業務知識といっても概要レベルで構いません。細かな部分は顧客とのヒアリングの中でイメージできれば良いので、ここでは大まかな業務知識を持ち合わせておけば良いです。

業務知識を持ち合わせていないと、顧客が抱えている本質的な課題を捉えることが出来ません。さもなくば顧客から挙げられた具体的な要望を、そのまま要件として捉えてシステムに反映させてしまいます。顧客はシステム開発のプロではないので、要件に矛盾が生じたり、システムとしての一貫性が欠けてしまい、カオスなシステム出来上がってしまいます。

顧客の要望を一人称視点で理解することで、適切な要件に落とし込むことが出来きます。 顧客から挙げられた要望を、グルーピングなどの作業を通じて抽象化を行います。顧客がどのような課題に悩んでいて、どのように改善したいのか。そのためにはどのようなシステムを作ればよいのかを考えていきます。

これらを整理すると、顧客の要望から要件に落とし込む手順を以下の通りになります。

  1. 顧客と同レベルの業務知識を持ち合わせる
  2. 顧客の要望をヒアリングする(具体レベルの内容であることが多い)
  3. 顧客の要望からグルーピングなどの抽象化作業を行い、要件に落とし込む

ワイヤーフレームで顧客と認識を合わせる

要件の成果物としてワイヤーフレームを作成します。 要件に落とし込んだ抽象的な内容をワイヤーフレーム通じて画面という形に具現化します。

実際にワイヤーフレームを作成してみると、抽象的だった要件が段々と具体化されていくことに気付くかと思います。具体化する作業の中で様々な疑問が生じてきます。 それらの疑問を「課題管理表」というシートに全て記載して顧客とのミーティングの中で解決させていきます。

一通り作成すると要件がブラッシュアップされ、より実装イメージが湧いてきます。 ワイヤーフレームを顧客にレビューしてもらい、フィードバックを得るという作業を繰り返すことで、さらにブラッシュアップされていきます。

完成物イメージを通じて、顧客と要件の齟齬を無くすことで、相互のイメージ不一致、いわゆる「顧客が本当に必要だったもの」のリスクを潰すことができます。

ワイヤーフレームを作成にはfigmaなどのツールを用いることをおすすめします。 figmaを使うと部品の共通化などが行えるので、顧客のフィードバックを反映するのも比較的容易です。 また、画面遷移の機能も備わっていますので、実物をイメージしてもらいやすいのもメリットの一つです。

洗い出した機能をベースにテーブル設計を行う

ワイヤーフレームが確定すると必要な機能が一通り洗い出されたことになります。この時点でテーブル設計を行います。

テーブル設計の後に画面設計(ワイヤーフレーム作成)を行ったほうが良いという考えもありますが、ワイヤーフレーム=顧客との仕様の確定作業と考えると、テーブル設計を後に回したほうが手戻りが少なくなります。そのため私は画面設計、テーブル設計の順番で進めることを推奨します。

テーブル設計にはdiagrams.net (旧draw.io)の「ソフトウェア > Database3」のプリセットを使ってER図を作成することをおすすめします。diagrams.net使うと各テーブル間の関係線やキーの属性などを直感的に設定できるので、とてもスピーディーにER図を作成することが出来ます。

操作性や非機能要件を元に技術を選定する

ワイヤーフレーム作成を終えると、画面の操作感や複雑性を把握することが出来ます。また、テーブル設計を終えるとシステムの規模感やデータ量が見えてきます。

これらの情報を元に、開発で使用する技術を選定します。

言語、フレームワーク、フロントエンド周り、バックエンド周り、ミドルウェア、データベース、その他ソフトウェアなどを決める必要があります。

チームのスキルセットやシステムの保守性、非機能要件と照らし合わせて、最適な技術を選定します。

例えば、インタラクティブな画面が多い場合は、SPAベースのフロントエンドを検討します。また、大量データを扱う必要がある場合は、スシャーディングできるデータベースを検討したり、NoSQLの導入なども検討します。

チームのスキルセットも重要な指標です。習熟していない言語や技術を採用するのはリスクを伴いますので、開発難易度とのバランスをみて新たな技術を導入するかどうか判断すると良いでしょう。

粗い粒度でタスク化する

技術選定が終えると、いよいよタスク化できる材料が揃います。

タスク化の粒度は「ログイン機能の作成」「トップ画面の作成」のように機能単位や画面単位のように粗くで構いません。

実際にそのタスクに取り掛かるタイミングで具体的なタスクの細分化を行います。 タスク(親)に対して複数のタスク(子)を持たせる形で細分化を行います。

ZenHub(Githubのプロジェクト管理用アドオン)でいうところの、親タスクをepic、子タスクをissueのような関係です。

システム全体の進捗管理は親タスク(epic)で行います。個々の機能単位での進捗管理は子タスク(issue)で行います。こうすることでユーザー目線とエンジニア目線の両方でスケジュール管理ができます。

開発環境、ステージング環境は真っ先に構築する

最も優先度が高いタスクは、開発環境(ローカル)とステージング環境(クラウド)の構築です。

ステージング環境構築は後回しにしがちですが、ローカルとクラウドでは、環境制約によって実装に違いが生じるリスクがあります。 開発環境に依存した実装になってしまい、そのままのコードではステージング環境では動かないという自体になりかねませんので、なるべく早めにステージング環境を構築することをおすすめします。

ひたすらタスクを消化する

ここまで来れば、あとは黙々とタスクを消化していきます。

タスクを消化するフェーズは、その上段にある要件定義、設計、タスク化、環境構築という土壌があって成り立ちます。 土壌がしっかりしていないとタスクはうまく消化できません。

土壌の組み立ては時間がかかる作業ですが、しっかりと思考することで高い保守性が実現できますので、腰を据えて望みましょう。

Heroku Pipelineでデプロイ作業がかなりラクになった

今まで同じコードベースを複数のアプリで別管理していたのですが、さすがに片手で数えられないアプリ数にもなると管理が辛くなってきたので、Heroku Pipelineを導入してみました。

実際に使用してみると、痒いところに手が届く素晴らしい機能だと感じたので紹介したいと思います。

ステージごとに「複数」のアプリを載せられる

Heroku PipelineではReview App、Staging、Productionの3つのステージが存在します。

Review AppにはPR単位の使い切りの一時的なアプリを載せて、StagingとProductionにはそれぞれ該当のアプリを登録します。

StagingとProductionそれぞれアプリは1つのみ載せられると勝手に思い込んでいましたが、複数のアプリを載せられるという仕様なのは「さすがHeroku」だと感じました。

例えばパッケージソフトを扱うシステムの場合、Staging環境に「開発用アプリ」「デモ用アプリ」、Productiion環境に「A社用アプリ」「B社用アプリ」のように複数のアプリを登録することが可能です。

「開発用アプリ」のスラグを一括で「A社用アプリ」「B社用アプリ」にプロモートすることができます。これによりリリース作業の煩わしさがなくなり、とても管理が楽になりました。

また、Herokuではプロモートやデプロイ時に実行される「リリースフェーズ」に特定のコマンドを発行することができます。

Procfileに以下のように「release」の定義をすることで、今までコマンドで直接叩いていたマイグレーションも自動化できるようになりました。 マイグレーションをし忘れるというリスクもなくなり至れり尽くせりです。

Procfile

release: bin/rails db:migrate
web: bundle exec puma -C config/puma.rb

自動テストがHeroku CIで簡単に実行できる

有料にはなりますが、Heroku CIを利用することでRSpecやRubocopなどのテスト環境を簡単に構築することができます。

CIツールというと有名所だとCircle CIなどがあります。Circle CIの場合、Docker上にテスト環境を構築してテストを実行する必要があります。

シンプルにRSpecとRubocopを実行したいだけでも、それなりに複雑なDockerの定義が必要になります。 (例えばimageを定義したり、dbを構築したり、各種ライブラリをインストールしたりなど)

Heroku CIの場合、Herokuのdyno上で実行されるため、Dockerの定義が必要ありません。そのため、設定ファイルはかなりシンプルな形式になります。

app.json

{
  "environments": {
    "test": {
      "addons": [
        "heroku-postgresql"
      ],
      "scripts": {
        "test": "bundle exec rubocop app && bundle exec rspec spec"
      }
    }
  }
}

ただし、複雑な自動テストをしたい場合にはHeroku CIは向いていないかもしれません。

例えば、数回に1回何らかの理由で失敗してしまうテストを抽出して、再テストするような自動化を構築する場合、これをHeroku CI再現させるのは難しいように感じました。 (そもそもそのようなテストがある事自体が問題なのですが。。)

Circle CIではworkflowという機能を使って、テストjobと再テストjobをコントロールして失敗したテストを再テストするということが可能ですが、Heroku CIでは一筋縄では実現できなそうでした。(もしかすると方法があるのかもしれませんが)

とはいえほとんどのケースはHeroku CIで実現できますので、一考の価値はあるかと思います。

Railsで簡単にPDFが出力できるGroverを導入してみた(Herokuへのデプロイも)

この記事では、Groverを使ってPDFを出力する際の導入方法や注意点を記載しております。また、Herokuへのデプロイについても説明します。

Groverとは?

GroverとはGoogle PuppeteerとChromiumを使ってHTMLをPDFや画像ファイル変換するGemです。

RailsでGroverを使うと、HTML(ERB)を実装するのと同じ感覚で、簡単にPDF出力ロジックを実装することが出来ます。

github.com

Google Puppeteerとは?

スクリプトを介してヘッドレストブラウザ(GUIを持たないブラウザ)や通常のブラウザを操作することができるフレームワークです。主にブラウザの自動テストなどに使用されます。類似の代表的なフレームワークにSeleniumなどがあります。

developer.chrome.com

Chromiumとは?

Chromeのベースとなるブラウザエンジンです。Chromeとの違いは自動アップデートやクラッシュレポートなどの機能が搭載されていない点です。

Grover、Puppeteer、Chromiumの関係

Grover、Puppeteer、Chromiumの関係について整理します。

  • ① GroverはPuppeteerとChromiumを元にHTMLを描画します。
    • 実際にはPuppeteerがChromiumを使用してブラウザを操作してHTMLを描画します。
  • ② Groverは描画されたHTMLを元に画像ファイル(PDF、PNG、JPEG)を生成します。

Grover、Puppeteer、Chromiumの関係

実装イメージ

インストール

Gemfileにgroverを追加します。

Gemfile

gem 'grover'

設定ファイル

initializersにGroverの共通定義を設定します。 実際の出力処理で個別に上書きすることも可能です。

config/initializers/grover.rb

Grover.configure do |config|
  config.options = {
    format: 'A4',
    margin: {
      top: '30px',
      right: '20px',
      left: '20px'
    },
    cache: false,
    display_url: <%= ENV.fetch("GROVER_DISPLAY_URL") {'localhost:3000' } %>
  }
end

display_urlには自ホストのアドレスを入力してください。これは出力対象のHTMLでCSSファイルを読み込んでいる場合に必要になります。デプロイを想定して環境変数などする必要があります。

HTMLファイル内に直接スタイルを定義している場合、display_urlは不要です。

PDF出力コントローラーの実装例

  1. ActionController::Baseに定義されているrender_to_stringを使用して、指定したtemplateからHTML構文を生成します。
  2. HTML構文をGroverに渡して、to_pdfメソッドでPDFを生成します。
  3. send_dataでPDFファイルをクライアントに返します。
def show
  html = self.class.new.render_to_string(template: 'sample/index')
  pdf = Grover.new(html).to_pdf
  send_data(pdf, filename: 'sample.pdf', type: 'application/pdf', disposition: 'inline')
end

Herokuで使う場合の注意点

Herokuにデプロイする場合はいくつか注意点があります。

buildpackの追加

Herokuで使用するためにはNode.jsとpuppeteerのビルドパックを追加する必要があります。

Node.jsビルドパックの追加

heroku buildpacks:add heroku/nodejs --index=1

puppeteer-heroku-buildpackの追加

heroku buildpacks:add jontewks/puppeteer --index=2

puppeteerのビルドパックの実態はpuppeteer-heroku-buildpackです。このビルドパックにはHerokuサーバー上でpuppeteerを使用するために必要なライブラリ群が含まれています。

しかし、このビルドパックを導入するとSlug Size(Herokuの容量)をかなり消費することになります。HerokuのSlug Sizeは最大500MBまでなのですが、puppeteerを導入すると余裕で400MBは超えてしまうでしょう。

その原因はpuppeteer-heroku-buildpackに含まれているxdg-utilsというライブラリの影響が大きいと思います。このライブラリだけで344MB消費しています。

そのため、puppeteer-heroku-buildpackのGithubではxdg-utilsを除いたタグ(22.0.0-no-xdg-utils)も作成されているようです。xdg-utilsを除くことによる影響は100%ないとは保証できないとのことですので、導入は自己責任でお願いします。

ちなみに私の環境では特に問題なかったので22.0.0-no-xdg-utilsをビルドしています。

heroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack#22.0.0-no-xdg-utils --index=2

GROVER_NO_SANDBOXの設定

GROVER_NO_SANDBOX=trueという環境変数を設定する必要があります。

no_sandboxはChromiumに対して適用されるようです。

そもそもChromiumのサンドボックスとはタブ毎に独立したプロセスでアプリケーションを稼働する仕組みをさします。デフォルトではこれが有効になっていますのが、Herokuで利用するためには無効にする必要があります。

heroku config:set GROVER_NO_SANDBOX=true

日本語フォントの追加

Herokuは日本語フォントに対応しておらず、そのまま出力しようとすると文字化けしてしまいます。

そのため、日本語フォントをHerokuに読み込ませる必要があります。

文字情報技術促進協議会のサイトからIPAフォントをダウンロードして、任意のttfファイルをプロジェクトのルートに配置します。

IPAexゴシックを使用する例

.fonts/
  - ipaexg.ttf

終わりに

PDF出力用のGemはいくつかありますが、 Groverを使用するとHTMLを実装する感覚でPDFレイアウトを組むことができるので保守性の観点でもおすすめです。

Groverのデメリットは処理が重くなりやすい点だと思います。シンプルなレイアウトであれば数秒で作成されますが、複数ページなどを出力する場合は処理時間の問題が発生します。

Herokuには30秒でタイムアウトしてしまうという仕様がありますので、要件によってはバックグラウンド(非同期処理)でPDFを作成して、完了したらクライアントにダウンロードさせるなどの考慮が必要になるかと思います。

Sidekiqでスタータスチェックや進捗管理ができるsidekiq-statusの使い方

Sidekiqのステータスチェックや進捗状況管理に便利なsidekiq-statusの使い方について説明します。

sidekiq-statusとは

sidekiq-statusを使うとジョブのスタータスや進捗状況を簡単に把握することができます。

sidekiq-statusはSidekiqのジョブに関する追跡情報をredis上に保存します。 クライアントからはjob_idを用いて簡単にそれらの情報を取得することができます。

例えば、重たい非同期処理(ジョブ)を画面から実行して、画面上にステータスや進捗情報を表示したい場合などに役立ちます。

sidekiq-statusの導入

sidekiq-statusのインストール

Gemfile

gem 'sidekiq-status'

sidekiq-statusの設定を追加

config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  # 下記を追記
  Sidekiq::Status.configure_server_middleware config, expiration: 30.minutes
  Sidekiq::Status.configure_client_middleware config, expiration: 30.minutes
end

Sidekiq.configure_client do |config|
  # 下記を追記
  Sidekiq::Status.configure_client_middleware config, expiration: 30.minutes
end

expirationにはステータスをredisに保持する期間を指定します。デフォルトでは30分です。

expirationはジョブクラスごとに上書きすることも可能です。ジョブの実行時間よりも長い時間を指定する必要があります。

ジョブクラスにSidekiq::Status::Workerをinclude

ステータス管理したいジョブクラスにSidekiq::Status::Workerをincludeすることでステータスを確認することができます。

class SampleJob
  include Sidekiq::Job
  include Sidekiq::Status::Worker # 追記します

  def perform(*args)
  end
end

ジョブステータス用の管理画面を追加

sidekiq-statusではsidekiqの管理画面にスタータス情報を表示する拡張機能を提供しています。

routes.rbに下記の記載をするとstatusesタブが新たに追加されます。

config/routes.rb

require 'sidekiq/web'
require 'sidekiq-status/web' # 追記する

statusesタブ

sidekiq-statusで出来ること

スタータスの把握

Sidekiq::Status::statusを使用することでスタータスを把握することが出来ます。

job_id = SampleJob.perform_async
status = Sidekiq::Status::status(job_id)

# statusは下記のいずれか
# queued,working,retrying,complete,failed,interrupted

# スタータス毎のメソッドも用意されている
Sidekiq::Status::queued? job_id
Sidekiq::Status::working? job_id
Sidekiq::Status::retrying?  job_id
Sidekiq::Status::complete?  job_id
Sidekiq::Status::failed? job_id
Sidekiq::Status::interrupted? job_id

注意点

statusにnilやfalseが返却されることがあります。 これはexpirationを超過している場合や、何らかの理由でSidekiqやredisの情報が消失された場合などに発生します。

この場合、画面上では失敗やエラーとして表現するなどの工夫が必要になります。

進捗管理

以下の様に進捗率を設定したり、自前の状態を保持することが可能です。

class SampleJob
  include Sidekiq::Job
  include Sidekiq::Status::Worker

  def perform(*args)
    total 100 # 進捗率の最大値

    # storeを使うと任意の状態(hoge_statusとする)を保持することができる
    store hoge_status: 'start'
    # retrieveでhoge_statusの状態を取得できる
    progress = retrieve :hoge_status
    p progress

    # 5秒待機(なんらかの処理を想定)
    sleep 5

    # 進捗率を50%に変更
    at 50, 'complete import'
    # hoge_statusの状態をimportedに変更
    store hoge_status: 'imported'

    # 5秒待機(なんらかの処理を想定)
    sleep 5

    # 進捗率を100%に変更
    at 100, 'finish'
    # hoge_statusの状態をfinishに変更
    store hoge_status: 'finish sample'
  end
end

クライアントからステータスを確認するには以下のメソッドを用います。

job_id = SampleJob.perform_async
Sidekiq::Status::get job_id, :hoge_status #=> 'start'
Sidekiq::Status::at job_id #=> 0

# 10秒後
Sidekiq::Status::total job_id #=> 100
Sidekiq::Status::message job_id #=> "finish"
Sidekiq::Status::get job_id, :hoge_status #=> 'finish sample'

管理画面からもステータスや進捗状況が確認できます。

管理画面から見た進捗率