Active Jobによる非同期実行

6 min read読了の目安(約5800字

Active Jobは非同期実行処理機能を提供するライブラリです。
Rails4.2から追加されました。

時間がかかる処理により、リクエストからレスポンスを返すまでの時間が長くなってしまうときには、先にレスポンスを返しておき、長い時間がかかる処理を非同期に別途実行することで、ユーザーに早く応答することができます。

ジョブの実装

非同期でコンソールにメッセージを表示する機能を作ってみる。

/app# bin/rails g model async_display message
/app# bin/rails db:migrate
models/async_display.rb
class AsyncDisplay < ApplicationRecord
  def self.console(message)
    puts "#{message}"
  end
end

ジョブクラスを生成する

# bin/rails g job async_display
async_display_job.rb
class AsyncDisplayJob < ApplicationJob
  queue_as :default

  def perform(message: "hello async job!")
    AsyncDisplay.console(message: message)
  end
end

Railsコンソールで実験してみる

> AsyncDisplayJob.perform_later(message: "Yahoo!")

Enqueued AsyncDisplayJob (Job ID: ab336b37-85df-40b2-b0fc-15d9c12187e1) to Async(default) with arguments: {:message=>"Yahoo!"}
=> #<AsyncDisplayJob:0x00007fc32c3c9ff0 @arguments=[{:message=>"Yahoo!"}], @job_id="ab336b37-85df-40b2-b0fc-15d9c12187e1", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @provider_job_id="45512861-0415-49ba-be0f-71241e7eff18">
Performing AsyncDisplayJob (Job ID: ab336b37-85df-40b2-b0fc-15d9c12187e1) from Async(default) enqueued at 2021-04-26T11:25:58Z with arguments: {:message=>"Yahoo!"}
{:message=>"Yahoo!"}
irb(main):008:0> Performed AsyncDisplayJob (Job ID: ab336b37-85df-40b2-b0fc-15d9c12187e1) from Async(default) in 0.26ms

15秒後にコンソールにメッセージを表示させてみる。

> AsyncDisplayJob.set(wait: 15.seconds).perform_later(message: "Yahoo!")

Enqueued AsyncDisplayJob (Job ID: f776af33-99cb-42e9-9ece-82b7fd6d75d7) to Async(default) at 2021-04-26 11:27:12 UTC with arguments: {:message=>"Yahoo!"}
=> #<AsyncDisplayJob:0x00007fc32c6404f0 @arguments=[{:message=>"Yahoo!"}], @job_id="f776af33-99cb-42e9-9ece-82b7fd6d75d7", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @provider_job_id="3b8e7d70-7027-4f59-bc98-3077183e9ce6", @scheduled_at=1619436432.650006>

## ... 15秒後 ...
> Performing AsyncDisplayJob (Job ID: f776af33-99cb-42e9-9ece-82b7fd6d75d7) from Async(default) enqueued at 2021-04-26T11:26:57Z with arguments: {:message=>"Yahoo!"}
{:message=>"Yahoo!"}
Performed AsyncDisplayJob (Job ID: f776af33-99cb-42e9-9ece-82b7fd6d75d7) from Async(default) in 0.2ms

Delayed Jobを実装する

Gemfileにdelayed_job_active_recordを追加してからbundle installを実行する。

Gemfile
gem 'delayed_job_active_record'
gem 'daemons'

バックエンドのジョブを実行するために必要なファイルを作成する。

/app# rails g delayed_job:active_record

すると/bin以下にdelayed_jobというファイルが生成される。

bin/delayed_job
#!/usr/bin/env ruby

require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
require 'delayed/command'
Delayed::Command.new(ARGV).daemonize

必要なテーブルを作成する。

/app# bin/rails db:migrate

テーブルを作成するとdelayed_jobsテーブルが作成されます。

create_table "delayed_jobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.integer "priority", default: 0, null: false
    t.integer "attempts", default: 0, null: false
    t.text "handler", null: false
    t.text "last_error"
    t.datetime "run_at"
    t.datetime "locked_at"
    t.datetime "failed_at"
    t.string "locked_by"
    t.string "queue"
    t.datetime "created_at", precision: 6
    t.datetime "updated_at", precision: 6
    t.index ["priority", "run_at"], name: "delayed_jobs_priority"
  end

delayed_jobの起動と停止

/app# bin/delayed_job start
delayed_job: process with pid 2093 started.
root      2093  1.4  3.7 631036 75692 ?        Sl   00:01   0:00 delayed_job

/app# bin/delayed_job stop

使ってみる

  • 命令を作る
  • 命令を非同期で実行する

という簡単なサンプルを作ってみる。

命令を入力して、実行する。

config/application.rbを編集し、以下を追記する。

config/application.rb
config.active_job.queue_adapter = :delayed_job
/app# bin/rails g controller Commands new create index
/app# bin/rails g model Command name:string

# bin/rails db:migrateでもOK
/app# bundle exec rake db:migrate

delayed_jobは非同期実行したいメソッドにdelayをつけて呼び出す。

app/controllers/commands_controller.rb
class CommandsController < ApplicationController
  def new
    @command = Command.new
  end

  def create
    puts "create called."
    command = Command.new(command_params)
    Command.delay.execute_command(command.name)
    redirect_to commands_url, notice: "命令「#{command.name}」を実行しました。"
  end

  def index
  end

  private

  def command_params
    params.require(:command).permit(:name)
  end
end
app/models/command.rb
class Command < ApplicationRecord

  def self.execute_command(name="hello")
    name << "_#{Time.now.strftime("%H%M%S")}"
    sleep rand(1..5)
    puts "Command [#{name}] を実行しました。"
  end
end
app/views/commands/new.html.erb
<h1>Create Command</h1>
<%= form_with model: @command, url: :commands, local: true do |f| %>

  name:<%= f.text_field :name %>
  <br/>
  <%= f.submit "登録" %>
<% end %>
app/views/commands/index.html.erb
<h1>Command#index</h1>
<% if flash.notice.present? %>
  <%= flash.notice %>
<% else %>
  命令はありませんよ
<% end %>

<%= link_to "命令を作る", commands_new_path %>

適当に命令を作ったあとで、rails consoleで確認してみる。

/app# rails c
> Delayed::Job.last.invoke_job
  Delayed::Backend::ActiveRecord::Job Load (0.9ms)  SELECT `delayed_jobs`.* FROM `delayed_jobs` ORDER BY `delayed_jobs`.`id` DESC LIMIT 1
Command [acdcd_002653] を実行しました。

参考

Rails4でサイトを構築する – 非同期処理編(delayed_job)
【Rails】delayed_jobのenqueue方法とqueue(非同期処理)実行方法についてまとめ