SidekiqでRailsに非同期処理を実装するチュートリアル


<img src="https://camo.qiitausercontent.com/608d02ddee3de4b894ec84cf4d5b555f6c894f31/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3137343332352f32306161666166322d353063372d666362622d643062352d6330386632653439666235322e706e67" width="300px">
(アイコンがイケてる...)

# はじめに

Sidekiqはバックグラウンドジョブ処理を制御するためのライブラリです。
Railsで重たい処理をバッチなどでキューに追加して、別のスレッドで実行するというのがSidekiqを用いると簡単にできます。
今の仕事でこれを使っており、調べて試してみたのでそれを記事にしました。

ここでは、ポケモンデータベースで有名な[pokedex](https://github.com/veekun/pokedex)を題材にして、Sidekiqの必要性、ワークフロー、実装の仕方をざっくりやってみます。

# つくってみるもの

[ここに](https://github.com/veekun/pokedex/blob/master/pokedex/data/csv/pokemon.csv)807匹のポケモンの情報を列挙したcsvファイルがあります。
これを元に、807個のPokemonクラスのインスタンスを生成して、保存して、テーブルで表示するWebアプリケーションを作るとします。

その場合、普通に書いたらpokemons_controllerは次のようになると思います。

```ruby
class PokemonsController < ApplicationController
  require 'csv'
  def index
    @pokemons = Pokemon.all
  end
  def upload
    csv_path = File.join Rails.root, 'db', 'pokemon.csv'
    CSV.foreach((csv_path), headers: true) do |pokemon|
      Pokemon.create(
        species_id: pokemon[2],
        name: pokemon[1],
        height: pokemon[3],
        weight: pokemon[4])
    end
    flash[:notice] = "All 807 Pokemons added to db."
    redirect_to pokemons_path
  end
  def destroy_all
    Pokemon.destroy_all
    flash[:notice] = "All 807 Pokemons deleted from db."
    redirect_to pokemons_path
  end
end
```

このコードは正常に動作しますが、例えばuploadが呼び出されると、同期的に処理されるため、807個のモデルオブジェクトの保存が終わるまでブラウザが読み込み中のまま固まってしまします。
さらにデータが増えれば、サーバの環境によっては`408 Request Timeout`が返されることもあるため、解消すべき問題です。


そこで、Sidekiqによるマルチスレッド処理を実装します。
メインのWebアプリケーションのスレッドで時間のかかるPokemonインスタンスの生成や削除、保存をするのではなく、Sidekiqにこれらを任せることができます。

処理の流れは次の図のようになります。

![](https://i.imgur.com/88SftlK.jpg)

モデルオブジェクトの保存が全て完了してから結果を画面に表示するのではなく、Redisにこのタスクをキューイングします。
Webアプリケーションは、タスクが実行中であることを、フラッシュメッセージでユーザーに伝えることができます。
その裏ではSidekiqによる処理が実行されています。

# チュートリアル

## Setup

SidekiqはキューをRedisに書き込むことでジョブ管理します。
Redisがインストールされていない場合は、インストールしてください。

```bash
$ brew install redis
```

## 同期的な処理のアプリケーション

まずは同期的な処理のPokemonアプリケーションを普通に作ってみます。
rails generateでサクッと実装します。

```bash
$ rails new sidekiq_pokemon
$ cd sidekiq_pokemon
$ rails g model Pokemon name:string species_id:integer height:integer weight:integer
$ bundle exec rake db:migrate
$ rails g controller pokemons index upload destroy_all --skip-template-engine # viewファイルの生成をスキップするオプション
$ mkdir app/views/pokemons
$ touch app/views/pokemons/index.html.erb
$ wget -O db/pokemon.csv https://raw.githubusercontent.com/veekun/pokedex/master/pokedex/data/csv/pokemon.csv # ポケモンのcsvをダウンロード
```

次に、`routes.rb`を以下のように編集します。

```ruby
Rails.application.routes.draw do

  resources :pokemon, only: [:index]
  post 'pokemons/upload', to: 'pokemons#upload'
  post 'pokemons/destroy_all', to: 'pokemons#destroy_all'
end
```

`pokemons_controller.rb`と`views/pokemons/index.html.erb`も以下のように編集します。

```ruby
class PokemonsController < ApplicationController
  require 'csv'
  def index
    @pokemons = Pokemon.all
  end
  def upload
    csv_path = File.join Rails.root, 'db', 'pokemon.csv'
    CSV.foreach((csv_path), headers: true) do |pokemon|
      Pokemon.create(
        species_id: pokemon[2],
        name: pokemon[1],
        height: pokemon[3],
        weight: pokemon[4])
    end
    flash[:notice] = "All 807 Pokemons added to db."
    redirect_to pokemons_path
  end
  def destroy_all
    Pokemon.destroy_all
    flash[:notice] = "All 807 Pokemons deleted from db."
    redirect_to pokemons_path
  end
end
```

```erb
<h1>Pokemons</h1>

<% if flash[:notice] %>
  <div class="notice"><%= flash[:notice] %></div>
<% end %>

<%#= link_to "See sidekiq stats", sidekiq_web_path %>

<%= form_tag pokemons_upload_path do %>
  <%= submit_tag "Import Pokemons" %>
<% end %>

<%= form_tag pokemons_destroy_all_path do %>
  <%= submit_tag "Delete Pokemons" %>
<% end %>

<table>
  <tr>
    <th>ID</th>
    <th>Pokemon</th>
    <th>Height</th>
    <th>Weight</th>
  </tr>
  <% @pokemons.each do |pokemon| %>
    <tr>
      <td><%=pokemon.species_id%></td>
      <td><%=pokemon.name%></td>
      <td><%=pokemon.height%></td>
      <td><%=pokemon.weight%></td>
    </tr>
  <%end%>
</table>
```

準備が整ったので`rails s`でサーバを起動し、http://localhost:3000/pokemons にアクセスします。

![](https://i.imgur.com/Fi36NBc.png)

`Import Pokemons`ボタンを押すと、807個のインスタンスを生成して保存するuploadアクションが開始され、最後にリダイレクトされて結果が表示されます。
`Delete Pokemons`はその逆です。

普通の実装ではどれくらいの時間を要するのか、実際にやってみます。

**upload**

![](https://i.imgur.com/0Qa6smJ.png)


**destroy_all**

![](https://i.imgur.com/u0T2f2O.png)

localhostでサーバを起動しているにもかかわらず結構かかっています。
herokuなどのサーバにデプロイした場合、これよりかなり時間がかかることが予想できます。


## Sidekiqによる非同期処理の実装

これまでのコードを、Sidekiqによるマルチスレッド処理に対応させるためにリファクタリングします。

Gemfileに`gem 'sidekiq'`を追記して、`bundle install`してください。

Sidekiqの処理を各workerファイルも作成します。

```bash
$ rails g sidekiq:worker PokemonAdd # workerファイルとそのテストファイルが生成される
$ rails g sidekiq:worker PokemonRemove # 一度に複数は作れないっぽい？
```

`pokemon_add_worker.rb`と`pokemon_remove_worker.rb`は次のように実装します。

```ruby
class PokemonAddWorker
  include Sidekiq::Worker
  sidekiq_options retry: false
  require 'csv'
  def perform(csv_path)
    CSV.foreach((csv_path), headers: true) do |pokemon|
      Pokemon.create(
        species_id: pokemon[2],
        name: pokemon[1],
        height: pokemon[3],
        weight: pokemon[4]
      )
    end
  end
end

```

```ruby
class PokemonRemoveWorker
  include Sidekiq::Worker
  sidekiq_options retry: false
  def perform
    Pokemon.destroy_all
  end
end

```

このように、Sidekiqのクラスを宣言するには、`Sidekiq::Worker`をincludeします。

また、`sidekiq_options`を特に指定しなければ、Sidekiqは処理に失敗した場合、そのメソッドを再試行しますが、例えば決済処理のような再試行してほしくないケースでは、上記のように`retry: false`とすることでこれを防ぐことができます。

そして、外出ししたい処理は`perform`メソッドに記述します。
ボトルネックになるからメインのWebアプリケーションのスレッドで処理してほしくない部分は、ここに記述してSidekiqに委譲できます。


これで主なロジックは実装できたので。`pokemons_controller.rb`から次のように呼び出せるようリファクタリングします。

```ruby
class PokemonsController < ApplicationController
  def index
    @pokemons = Pokemon.all
  end
  def upload
    csv_path = File.join Rails.root, 'db', 'pokemon.csv'
    PokemonAddWorker.perform_async(csv_path)
    flash[:notice] = "Pokemons getting added to db"
    redirect_to pokemons_path
  end
  def destroy_all
    PokemonRemoveWorker.perform_async
    flash[:notice] = "Pokemons getting removed from db"
    redirect_to pokemons_path
  end
end
```

workerメソッドは。上記のように`WorkerClassName.perform_async(args)`で呼び出すことができます。


これですべての準備ができたので、ローカルで実行してみます。

以下のコマンドでredis-serverとsidekiqを起動してください。

```bash
$ redis-server /usr/local/etc/redis.conf
$ bunde exec sidekiq
```

rails serverを起動して、実際にバックグラウンドジョブとしてインスタンス生成/削除が生成できているかを確認してみます。

(GIFにカーソルを表示するようにするの忘れちゃった...)
**upload**

![upload.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174325/ce9a4a4f-b66d-b54e-1e30-e732816c778e.gif)

**destroy_all**

![destroy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174325/74c31abc-85e3-9cc1-76d4-9da7f46f0d1d.gif)

Sidekiqの実装前と比べて、リダイレクトまでの時間が非常に速いです。
どちらも、モデルオブジェクトの保存が完了する前にリダイレクトされていることがわかると思います。
リダイレクトとモデルオブジェクト保存の処理が別のスレッドで実行されており、ボトルネックだったジョブはSidekiqによりバックグラウンドで処理されています。

# おわり

以上、Sidekiqの使い方の簡易的な説明でした。
もっと詳しく全体的な情報を知りたいという方は、[こちら](https://qiita.com/nysalor/items/94ecd53c2141d1c27d1f)の記事が参考になると思います。
例えば、Sidekiqは強力なタスク監視のダッシュボードがビルトインされており、それの設定の仕方などが丁寧に書かれています。

# 参考

[ソースコードとかの参考](https://itnext.io/sidekiq-overview-and-how-to-deploy-it-to-heroku-b8811fea9347)
[すごくわかりやすい公式wiki](https://github.com/mperham/sidekiq/wiki)