🏐

Rails Active Job をとりえず使ってみる (with docker-compose)

2021/03/25に公開約11,200字

まずはキューイングバックエンドなしで Active Job を使ってみます。

TL;DR

この記事でやること、やらないこと

  • この記事でやること
    • docker-compose で Rails コンテナと MySQL コンテナを用意
      • 今後、キューイングバックエンドを導入するときに Docker でやる下準備
    • キューイングバックエンドなしで Active Job を使ってみる
    • キューイングバックエンドとは、Job のキューpushとキューpopをしてくれる処理のこと
  • この記事でやらないこと
    • Sidekiq などのキューイングバックエンドの導入

Active Job で何をさせるか

こんな画面を用意し、
call job ボタンを押すと非同期で3秒感覚で何度か status がインクリメントされる処理を作ります。
非同期で status がインクリメントされる様子は、 update ボタンを押すことで view が再読み込みされるのでそれで確認できます。

docker-compose の準備, bundle init して rails new してよく使うgemも入れておく

前述の過去記事と同じですが手順を追ってサクッとやっていきましょう。

cd myapp
mkdir -p forDocker/mysql/conf.d/
touch forDocker/mysql/conf.d/mysql.cnf
mkdir -p forDocker/rails/
touch forDocker/rails/entrypoint.sh
touch Dockerfile
touch docker-compose.yml
forDocker/mysql/conf.d/mysql.cnf
[mysqld]
default_authentication_plugin = mysql_native_password
skip-host-cache
skip-name-resolve

character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = SET NAMES utf8mb4
skip-character-set-client-handshake

[client]
default-character-set = utf8mb4

[mysqldump]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4
forDocker/rails/entrypoint.sh
#!/bin/bash
set -e

# Reference by https://matsuand.github.io/docs.docker.jp.onthefly/compose/rails/
# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
Dockerfile
FROM ruby:2.7
RUN set -x && curl -sL https://deb.nodesource.com/setup_14.x | bash -

RUN set -x && apt-get update -y -qq && apt-get install -yq less lsof vim default-mysql-client

RUN set -x && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

RUN set -x && apt-get update -y -qq && apt-get install -yq nodejs yarn

RUN mkdir /app
WORKDIR /app
COPY . /app

# Add a script to be executed every time the container starts.
COPY ./forDocker/rails/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
# ※docker-compose up で rails s するときはここのコメントを外す
# CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3.8'

services:
  app:
    container_name: app
    build: .
    tty: true
    stdin_open: true
    volumes:
      - .:/app
      - bundle_install:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db

  db:
    platform: linux/x86_64
    image: mysql:8.0
    container_name: db
    restart: always
    volumes:
      - ./forDocker/mysql/conf.d:/etc/mysql/conf.d
      - dbvol:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: "Asia/Tokyo"

volumes:
  bundle_install:
  dbvol:

では初期設定をしていきましょう。
この時点で作業ディレクトリを VSCode とかで開いておきながら以下ターミナルの作業を開始すると楽です。

# サービス用のポート(3000)を有効化し、ホスト側に割り当て可能にして起動
docker-compose run --rm --service-ports app bash
# bashログインできてもちょっと待つ(MySQLが立ち上がってくるのを待つ)
# mysql にパスワードなし root で接続できるか確認
mysql -u root -h db -e 'select version();'
# bundle config 確認 BUNDLE_APP_CONFIG: "/usr/local/bundle" なのを確認
bundle config
# Gemfile 作成
bundle init
# rails 追加 (Gemfile を直接編集してから bundle install でもOK)
bundle add rails --version '~> 6.1.3.1'
bundle install
# rails new
#   -B:bundle installしない, -S:sprocketsを組み込まない
#   -T:test::unitを組み込まない, -J:javascriptを組み込まない
#   -d myaql:データベースの種類, --force:ファイルが存在する場合に上書き
rails new . -B -S -T -J -d mysql --force
bundle install

OKです。今回はついでに simpacker を入れちゃいます。
ターミナルはそのままにしておいて、Gemfile に simpacker を追記して

Gemfile
...(前略)
gem "simpacker"
...(後略)

でまたターミナルで

# simpacker を入れる
bundle install
# simpakcer 初期化
rails simpacker:install

webpack は消します。
ターミナルはそのままにしておいて、 package.json の devDependencies はいったん空に。

package.json
{
  "private": true,
  "devDependencies": {
  }
}

rails simpacker:install すると npm で入ってしまうので
不要なファイルを消して yarn しなおします。
でまたターミナルに戻って

rm -rf node_modules/
rm package-lock.json
rm webpack.config.js
yarn install

これで余計な package のない Simpacker がセットアップできます。
git 管理しているときは .gitignore に /node_modules/* も追記しておきましょう。

ここまで終わったら次は Rails アプリケーションの準備を整えましょう
ターミナルはそのままにしておいて、 config/database.yml を編集します。

config/database.yml
...(前略)
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  url: mysql2://root:@db:3306
  # username: root
  # password:
  # host: localhost
...(後略)

config/database.yml の編集を行ったら、データベースの準備をしてrailsを起動してみます。
でまたターミナルに戻って

rails db:create
rails db:migrate
rails s -b "0.0.0.0"

http://localhost:3000/ で "Yay! You’re on Rails!" っていういつものアレが出ましたでしょうか。
OKですね。Ctrl+c でサーバを閉じましょう。また、

exit
docker-compose down

して一度コンテナ全部を落とします。

これで rails s が通るようになったので
次に Dockerfile と docker-compose.yml を変更して
docker-compose uprails s されるようにしておきます。

Dockerfile の末尾のコメントを外し、
docker-compose.yml に command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" を追記します。
こんな感じですね。

Dockerfile
FROM ruby:2.7
RUN set -x && curl -sL https://deb.nodesource.com/setup_14.x | bash -

RUN set -x && apt-get update -y -qq && apt-get install -yq less lsof vim default-mysql-client

RUN set -x && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

RUN set -x && apt-get update -y -qq && apt-get install -yq nodejs yarn

RUN mkdir /app
WORKDIR /app
COPY . /app

# Add a script to be executed every time the container starts.
COPY ./forDocker/rails/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3.8'

services:
  app:
    container_name: app
    build: .
    tty: true
    stdin_open: true
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app
      - bundle_install:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db

  db:
    platform: linux/x86_64
    image: mysql:8.0
    container_name: db
    restart: always
    volumes:
      - ./forDocker/mysql/conf.d:/etc/mysql/conf.d
      - dbvol:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: "Asia/Tokyo"

volumes:
  bundle_install:
  dbvol:

今後は docker-compose up -d で全体を起動すれば
puma が勝手に立ち上がってくれます。

docker-compose up -d
# 今後、railsコマンド実行などで app の bash にログインするにはこう
docker-compose exec app bash

Active Job で操作するデータを用意する

今回はシンプルに status という情報を持つ model:JobStatus を用意して
Active Job は非同期で status を Increment する処理にします。

model:JobStatus

id status 備考
1 1 status は増え続ける

というわけで今回はサクッと rails g scaffold で用意しちゃいましょう。

rails g scaffold JobStatus status:integer
rails db:migrate

はい、これで model,controller,view,route が用意できましたね。
こういうところは Rails は本当に便利です。

JobStatus を更新する job を作成する

あまりよくないネーミングですが今回は SampleJob という job を用意します。

rails g job sample
app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(*args)
    logger.debug "Sample Job Start."

    job_status = JobStatus.last

    if job_status.blank?
      logger.debug "no status error."
      return
    end

    for i in 0..4
      job_status.status += 1
      job_status.save!
      sleep(3) # 3秒待機
    end

    logger.debug "Sample Job End."
  end
end

特に難しいことはしていません。
JobStatus の最後のレコードを取得して
3秒待機しつつ 5回 status を Increment します。

JobStatus を更新する job を実行するエンドポイントを作成する

./home/call_job で 上記 SampleJob を実行するエンドポイントを作成します。
ついでなので root を置き換える view として home/index も作っちゃいましょう。

rails g Home index
app/views/home/index.html.erb
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

<p>
  <%= link_to 'to /job_statuses', job_statuses_path %>
</p>

home/index には ./job_statuses/index へのリンクを貼っておきました。

app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end

  def call_job
    SampleJob.perform_later
  end
end

SampleJob の実行は Active Job の実行に則って SampleJob.perform_later とします。これで非同期で SampleJob.perform が実行されます。
今回はキューイングバックエンドなしで Active Job を利用するので、
メモリ上にタスクがキューイングされたのち実行されます。

あとは config/routes.rb に追記しておきます。

config/routes.rb
Rails.application.routes.draw do
  resources :job_statuses
  # 以下を追記
  get 'home/index'
  get 'home/call_job'
  root to: 'home#index'
end

JobStatus を 1レコード作っておく

レコードがないと更新する対象がないので
http://localhost:3000/job_statuses から New Job Status
レコードを最低でも1つ作っておきます。

./job_statuses に job を実行する処理を追加する

準備が整ったので、
最後に ./job_statuses に job を実行する処理を追加します。
既に rails g scaffold で生成されている
app/views/job_statuses/index.html.erb に以下のように追記します。

app/views/job_statuses/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Job Statuses</h1>

<%# buttonを2つ追加 %>
<button type="button" id="reload">reload</button>
<button type="button" id="calljob">call job</button>

<table>
  <thead>
    <tr>
      <th>Status</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @job_statuses.each do |job_status| %>
      <tr>
        <td><%= job_status.status %></td>
        <td><%= link_to 'Show', job_status %></td>
        <td><%= link_to 'Edit', edit_job_status_path(job_status) %></td>
        <td><%= link_to 'Destroy', job_status, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Job Status', new_job_status_path %>

<%# ここから下を追加 %>
<script>
  var reload =document.getElementById('reload');
  reload.addEventListener('click',function(){
    window.location.reload();
  });

  var reload =document.getElementById('calljob');
  reload.addEventListener('click',function(){
    fetch('/home/call_job', {
      method: 'GET'
    });
  });
</script>

これも特に難しいことはしていないです。
button を2つ追加し、
それぞれに reload の処理と Fetch API で GET Request を投げる処理を記述してあります。
reload ボタンは別に必要ないのですが
calljob ボタンを押した後、3秒ごとに Status が Increment される様子を
確認しやすいように追加しました。

動かしてみる

call job ボタンを押すと非同期で3秒感覚で5回 status がインクリメントされます。
非同期で status がインクリメントされる様子は、 update ボタンを押すことで view が再読み込みされるのでそれで確認できます。
おつかれさまでした。

まとめ

キモは rails g job sample で作った処理を
SampleJob.perform_later で call してやるだけなので簡単ですね。
今回はキューイングバックエンドなしで Active Job を利用したので、
メモリ上にタスクがキューイングされたのち実行されます。
次は何かしらのキューイングバックエンドを導入した記事を書ければと思います。

今回のリポジトリはこちらです。

https://github.com/JUNKI555/rails_activejob_practice01

Discussion

ログインするとコメントできます