酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

ActiveJobの簡単な導入手順とRSpecの書き方について

ActiveJobの導入手順と、ActiveJob周りのRSpecの書き方をちゃんと理解していなかったので、英語の勉強も兼ねて、 『Active Job Basics — Ruby on Rails Guides』 を読んでみました!


ActiveJob とは

ActiveJobはジョブを宣言し、バックエンドでキュー操作を抽象化して実行するためのフレームワーク。

ジョブの作成

ジョブの作成コマンドはこちら。

1
2
3
4
$ bin/rails generate job guests_cleanup
invoke  test_unit
create    test/jobs/guests_cleanup_job_test.rb
create  app/jobs/guests_cleanup_job.rb

作成されたJobはこんな感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class GuestsCleanupJob < ActiveJob::Base
  queue_as :default # キューの名前

  # 例外発生時の処理を書く
  rescue_from(ActiveRecord::RecordNotFound) do |exception|
  end

  # ジョブをキューにセットする前に実行される
  before_enqueue do |job|
  end

  around_perform do |job, block|
    # ジョブが実行される前の処理
    block.call
    # ジョブが実行された後の処理
  end

  def perform(*guests)
    # 実際の処理を行う
  end
end

Callbackの種別

- before_enqueue => キューにセットする前に呼び出される 
- around_enqueue => キューにセットする前後で block を受け取り、呼び出す
- after_enqueue => キューにセットされた後に呼び出される 
- before_perform => ジョブが実行される前に呼び出される
- around_perform => ジョブが実行される前後で block を受け取り、呼び出す
- after_perform => ジョブが実行された後で呼び出される

ジョブの登録方法

1
2
3
4
5
6
7
8
9
10
# キューが無ければ、ジョブをすぐに実行する
GuestsCleanupJob.perform_later(guest)

# 明日の午後ジョブに登録する
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)

# 1週間後にジョブを実行する
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)

# perform_later も perform_now

設定

1
2
3
4
5
6
7
8
9
10
# config/application.rb
module YourApp
  class Application < Rails::Application
    # キューの名前の prefix
    config.active_job.queue_name_prefix = Rails.env

    # sidekiq や delayed_job 等、利用する job キューの仕組みを指定
    config.active_job.queue_adapter = :sidekiq
  end
end

RSpecの書き方

以下のようにテストできるそうです。

1
2
3
4
5
6
# spec/rails_helper.rb
RSpec.configure do |config|
  config.include ActiveJob::TestHelper
  config.include ActiveSupport::Testing::TimeHelpers
  ...
end
1
2
3
4
5
6
7
8
# app/jobs/notification_job.rb
class NotificationJob < ActiveJob::Base
  queue_as :default

  def perform(msg)
    # notification を送付
  end
end
1
2
3
4
5
6
# app/models/message.rb
class Message < ActiveRecord::Base
  def notification
    NotificationJob.set(wait: 5.seconds).perform_later body
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# spec/models/message_spec.rb
require 'rails_helper'

describe Message, '#notification' do
  let(:message) { Message.new(body: 'Hello world!') }

  it '5秒後に NotificationJob のキューにセットされる' do
    time = Time.current

    travel_to(time) do
      assertion = {
        job: NotificationJob,
        args: ['Hello world!'],
        at: (time + 10.seconds).to_i,
      }
      assert_enqueued_with(assertion) { message.ping }
    end
  end
end

(参考) GlobalID

ActiveJob以前は以下のようにclasss, idを渡してActiveRecordのオブジェクトを取得して、 からメソッドを実行していた。

1
2
3
4
5
6
class TrashableCleanupJob < ActiveJob::Base
  def perform(trashable_class, trashable_id, depth)
    trashable = trashable_class.constantize.find(trashable_id)
    trashable.cleanup(depth)
  end
end

現在はこんな風に書いておくだけでActiveJob側で良しなに対応してくれる。

1
2
3
4
5
class TrashableCleanupJob < ActiveJob::Base
  def perform(trashable, depth)
    trashable.cleanup(depth)
  end
end

これは、Rails4.2から実装された #gid#sgid の効果によるもの。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gid = Friend.first.gid
 => #<GlobalID:0x007fa9add041f8 ...>
gid.app
 => "activejob-example"
gid.model_name
 => "Friend"
gid.model_class
 => Friend(id: integer, name: string, email: string...)
gid.model_id
 => "1"
gid.to_s
 => "gid://activejob-example/Friend/1"

sgid = Friend.first.sgid
 => #<SignedGlobalID:0x007fa9add15e58 ...>
sgid.to_s
 => "BAh7CEkiCGdpZAY6BkVUSSIlZ2lkOi8vYWN0aXZl..."

これは知らなかったけど便利な機能。

(参考) Active Job adapters

Active Job adaptersの例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|-------------------|-------|--------|------------|------------|---------|---------|
|                   | Async | Queues | Delayed    | Priorities | Timeout | Retries |
|-------------------|-------|--------|------------|------------|---------|---------|
| Backburner        | Yes   | Yes    | Yes        | Yes        | Job     | Global  |
| Delayed Job       | Yes   | Yes    | Yes        | Job        | Global  | Global  |
| Qu                | Yes   | Yes    | No         | No         | No      | Global  |
| Que               | Yes   | Yes    | Yes        | Job        | No      | Job     |
| queue_classic     | Yes   | Yes    | Yes*       | No         | No      | No      |
| Resque            | Yes   | Yes    | Yes (Gem)  | Queue      | Global  | Yes     |
| Sidekiq           | Yes   | Yes    | Yes        | Queue      | No      | Job     |
| Sneakers          | Yes   | Yes    | No         | Queue      | Queue   | No      |
| Sucker Punch      | Yes   | Yes    | No         | No         | No      | No      |
| Active Job Inline | No    | Yes    | N/A        | N/A        | N/A     | N/A     |
|-------------------|-------|--------|------------|------------|---------|---------|

(参考) ActionMailer との連携

ActionMailerの #deliver_later は内部でActiveJobを使っている。

1
2
3
4
5
# すぐに送付する場合は、 #deliver_now
UserMailer.welcome(@user).deliver_now

# ActiveJobを使って後ほど送る場合は、 #deliver_later
UserMailer.welcome(@user).deliver_later

(参考) ActionMailerの #deliver_later に関する処理

rails/message_delivery.rb at v5.0.0.beta3 - rails/rails

Special Thanks

おすすめの書籍