Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Active Jobスタイルガイド(翻訳)

概要

CC BY 3.0ライセンス(Attribution 3.0 Unported)に基づいて翻訳・公開いたします。

Rails: Active Jobスタイルガイド(翻訳)

本スタイルガイドは、バックエンドにSidekiqを用いたActive JobでRubyバックグランドジョブを扱うときのベストプラクティスを一覧できるようにしたものです。

一般に思われているのと異なり、このガイドラインに沿うことでかなりうまくやれるようになります。

SidekiqはActive Jobなしでも使えますが、Active Jobは透過性や有用なシリアライゼーション層を追加してくれます。

このスタイルガイドは、何の根拠もないところから現れたのではありません。編集者のプロ開発者としての経験、公式ドキュメント、そしてRubyコミュニティメンバーによる提案を下敷きにしています。

このガイドラインは、さまざまな落とし穴を避けるのに役立ちます。バックグラウンドジョブの使い方によって適しているガイドラインもあれば、そうでないものもあります。

このガイドのPDF版はAsciiDoctor PDFで生成できます。また、AsciiDoctorを使って以下のコマンドを実行すれば、HTML版も生成できます。

# README.pdfを生成する
asciidoctor-pdf -a allow-uri-read README.adoc

# README.htmlを生成する
asciidoctor
ヒント
生成されたドキュメントにいい感じのシンタックスハイライトを付けたい場合は、rouge gemをインストールしてください。
gem install rouge

🔗 一般に推奨される方法

🔗 引数にはActive Recordモデルを渡すこと

引数にはActive Recordモデルを渡し、idを渡さないこと。Active JobはGlobalIDを用いて自動的にActive Recordモデルをシリアライズ/デシリアライズするので、モデルを手動でデシリアライズする必要はありません。

GlobalIDは、モデルクラスのミスマッチを正しく扱えます。

デシリアライズでエラーが発生すると、エラートラッキングに出力されます。

# bad: idで渡す
# デシリアライズエラーは出力されるが、このジョブはリトライ用にスケジュールされてしまう
class SomeJob < ApplicationJob
  def perform(model_id)
    model = Model.find(model_id)
    do_something_with(model)
  end
end

# bad: モデルのミスマッチ
class SomeJob < ApplicationJob
  def perform(model_id)
    Model.find(model_id)
    # ...
  end
end

# 別のモデルクラス(つまりユーザーのid)でModelをフェッチしようとする
SomeJob.perform_later(user.id)

# id渡しが容認される場合
# デシリアライズエラーは出力され、ジョブはリトライ用にスケジュール「されない」
class SomeJob < ApplicationJob
  def perform(model_id)
    model = Model.find(model_id)
    do_something_with(model)
  rescue ActiveRecord::RecordNotFound
    Rollbar.warning('Not found')
  end
end

# good: GlobalIDで渡す
# デシリアライズエラーは出力され、ジョブはリトライ用にスケジュール「されない」
class SomeJob < ApplicationJob
  def perform(model)
    do_something_with(model)
  end
end
警告
あるスタイルから別のスタイルにいきなり切り替えないこと。移行期間を設け、処理待ちとしてスケジュールされている、idを用いるジョブがすべて完了するまで待つこと。何らかのヘルパーを用いて、一時的に数値の引数とGlobalID引数を両方サポートすること。
class SomeJob < ApplicationJob
  include TransitionHelper

  def perform(model)
    # TODO: 数値id引数を持つジョブがすべて処理完了したら削除する
    model = fetch(model, Model)
    do_something_with(model)
  end
end

module TransitionHelper
  def fetch(id_or_object, model_class)
    case id_or_object
    when Numeric
      model_class.find(id_or_object)
    when model_class
      id_or_object
    else
      fail "Object type mismatch #{model_class}, #{id_or_object}"
    end
  end
end

🔗 キューの代入

ジョブのクラスで使われるキューを明示的に指定すること。そのキューが処理完了済みキューリストに乗っていることを確認すること。

多くのジョブをいっぺんにひとつの籠に押し込めると、緊急性の高いジョブが著しく遅延するリスクが生じます。実行に時間のかかるジョブと実行がすぐ終わるジョブを1つのキューに一緒に入れないこと。緊急性の高いジョブと、緊急性の低いジョブを1つのキューに一緒に入れないこと。

# bad: キューを指定してない
class SomeJob < ApplicationJob
  def perform
    # ...
  end
end

# bad: 指定するキューが間違っている
class SomeJob < ApplicationJob
  queue_as :hgh_prioriti # 存在しないキューを指定している

  def perform
    # ...
  end
end

# good
class SomeJob < ApplicationJob
  queue_as :high_priority

  def perform
    # ...
  end
end

🔗 冪等性(idempotency)

理想としては、ジョブを冪等に作るべきです。つまりそのジョブが2回以上実行されても悪い副作用が生じないように作るべきです。Sidekiqは、ジョブが少なくとも1回実行されることだけを保証しますが、正確に1回きり実行することについては必ずしも保証しません。

仮にジョブがエラーで落ちないとしても、ローリングリリースでないデプロイの実行中に割り込みが入る可能性があります。

参考: ローリング・リリース - Wikipedia

class UserNotificationJob < ApplicationJob
  def perform(user)
    send_email_to(user) unless already_notified?(user)
  end
end

🔗 原子性(atomicity)

デプロイ中のジョブには、完了のための時間がデフォルトで25秒間与えられます。これを超えると、ワーカーは終了してジョブがキューに戻されます。これによって、作業の一部が2回実行される可能性もあります。

ジョブはアトミックに作ること。つまり「成功か何もしないか、そのどちらかしか起きない」ように作ります。

🔗 スレッド

ジョブの内部でスレッドを使わないこと。その代わりに、ジョブ内部から別のジョブを起動すること。ひとつのジョブ内でスレッドを立ち上げると、新しいデータベースコネクションがオープンされることになり、このコネクションはWebサーバーがダウンするまで増え続けて簡単に枯渇してしまいます。

# bad: コネクションがすべて消費されてしまう
class SomeJob < ApplicationJob
  def perform
    User.find_each |user|
      Thread.new do
        ExternalService.update(user)
      end
    end
  end
end

# good
class SomeJob < ApplicationJob
  def perform(user)
    ExternalService.update(user)
  end
end

User.find_each |user|
  SomeJob.perform_later(user)
end

🔗 リトライ

Active Jobにビルトインされているretry_onActiveJob::Retryactivejob-retry gem)を使わないこと。Sidekiqのリトライ機能を使うこと(Sidekiq 6以降のリトライ機能はActive Jobからも利用可能)。

ジョブのリトライメカニズムを隠蔽したり切り出したりしないこと。リトライの指示はジョブの中で見えるようにしておくこと。

# bad: Rollbarに送信しないで3回リトライしている
# これは失敗し、Sidekiqのリトライに依存して
# リトライが何度も発生し、失敗のたびにRollbarに送信される
class SomeJob < ApplicationJob
  retry_on ThirdParty::Api::Errors::SomeError, wait: 1.minute, attempts: 3

  def perform(user)
    # ...
  end
end

# bad: ジョブがリトライかそうでないかがはっきりしない
class SomeJob < ApplicationJob
  include ReliableJob

  def perform(user)
    # ...
  end
end

# good: Sidekiqがリトライを担当している
class SomeJob < ApplicationJob
  sidekiq_options retry: 3

  def perform(user)
    # ...
  end
end
🔗 バッチ

バッチで実行されるジョブには必ずリトライ処理を付けること。さもないと、そのバッチは決して成功しません。

🔗 リトライを使おう

リトライのメカニズムを活用すること。ジョブをデッドジョブにしないこと。ジョブのリトライにSidekiqを使うようにし、手動でジョブを実行するために時間を消費しないこと。

🔗 トランザクションに配慮する

スケジュールされたジョブのバックグラウンド処理は、思ったより早い時期に始まる可能性があります。トランザクションのコミットが完了したジョブだけをスケジュールすること

# bad: トランザクションがコミットされる前にジョブが始まる可能性がある
User.transaction do
  users_params.each do |user_params|
    user = User.create!(user_params)
    NotifyUserJob.perform_later(user)
  end
end

# good
users = User.transaction do
          users_params.map do |user_params|
            User.create!(user_params)
          end
        end
users.each { |user| NotifyUserJob.perform_later(user) }

🔗 ローカルでのパフォーマンステスト

Sidekiqのジョブは、Railsのオートリロード機能によって1つずつ実行されますが、この実行はパラレルではありません。これに惑わされる可能性があります。

Sidekiqは、eager_loadtrueにした環境で実行するか、以下のフラグを指定してこの振る舞いを回避した環境で実行すること。

EAGER_LOAD=true ALLOW_CONCURRENCY=true bundle exec sidekiq

🔗 重要なジョブ

バックグラウンドジョブの処理が(失敗したデプロイの最中や他のジョブがバーストしている最中などで)分単位で長引くと、ダウンすることがあります。

タイムクリティカルなジョブやミッションクリティカルなジョブは、インプロセスでの実行を検討すること。

🔗 ジョブ内のビジネスロジック

ビジネスロジックはジョブの中に置かず、外に切り出すこと。

# bad
class SendUserAgreementJob < ApplicationJob
# 不要なジョブがスケジューリングされることを回避するための
# 事前条件が満たされているかどうかをチェックする便利メソッド
  def self.perform_later_if_applies(user)
    job = new(user)
    return unless job.satisfy_preconditions?

    job.enqueue
  end

  def perform(user)
    @user = user
    return unless satisfy_preconditions?

    agreement = agreement_for(user: user)
    AgreementMailer.deliver_now(agreement)
  end

  def satisfy_preconditions?
    legal_agreement_signed? &&
      !user.removed? &&
      !user.referral? &&
      !(user.active? || user.pending?) &&
      !user.has_flag?(:on_hold)
  end

  private

  attr_reader :user

  # business logic
end

# good: ビジネスロジックがジョブと癒着していない
class SendUserAgreementJob < ApplicationJob
  def perform(user)
    agreement = agreement_for(user: user)
    AgreementMailer.deliver_now(agreement)
  end
end

SendUserAgreementJob.perform_later(user) if satisfy_preconditions?

🔗 あるジョブから別のジョブへのスケジューリング

「ジョブから別のジョブをスケジュールすべきかどうか」あるいは「インプロセスで実行すべきかどうか」についてはそれぞれのメリットとデメリットを秤にかけること。考慮すべき要素:「そのジョブはリトライ可能か」「内側のジョブは失敗する可能性があるか」「ジョブは冪等か」「失敗する可能性のある親ジョブで何か他のことをやっているか」

# メリット:「エラーカーネル」パターン
# デメリット: 追加ジョブが起動される
class SomeJob < ApplicationJob
  def perform
    SomeMailer.some_notification.deliver_later
    OtherJob.perform_later
  end
end

# good: ジョブを追加しない
# bad: `OtherJob`が失敗すると`SomeMailer`もリトライで再実行される
class SomeJob < ApplicationJob
  def perform
    SomeMailer.some_notification.deliver_now
    OtherJob.perform_now
  end
end

🔗 ジョブがものすごく多い場合

多数のジョブを実行しなければならない場合は、スケジューリングするのも手です。

トレースしやすくするには、バッチの利用を検討すること。

また、ホスト(親)ジョブとサブジョブで同じキューを指定すること。

# 容認できる方法
def perform
  batch = Sidekiq::Batch.new
  batch.description = 'Send weekly reminders'
  batch.jobs do
    User.find_each do |user|
      WeeklyReminderJob.perform_later(user)
    end
  end
end

🔗 ジョブをリネームする

ジョブクラスのリネームは慎重に行うこと(ジョブがスケジュールされたタイミングでジョブを処理するクラスが存在しなくなる状況に陥らないようにすること)。

メモ
これはメイラーでdeliver_laterを使うときにも関連します。
# good: 古いクラスが維持される
# TODO: 古いジョブが無事に終わったら数週間以内にこのエイリアスを削除すること
OldJob = NewJob

🔗 sleep

ジョブ内でKernel.sleepを使わないこと。sleepするとワーカースレッドがブロックされ、他のジョブが処理不能になってしまいます。そのジョブをリスケして後で実行するか、リミッターでカスタム例外を用いること。

# bad
class SomeJob < ApplicationJob
  def perform(user)
    attempts_number = 3
    ThirdParty::Api::User.renew(user.external_id)
  rescue ThirdParty::Api::Errors::TooManyRequestsError => error
    sleep(error.retry_after)
    attempts_number -= 1
    retry unless attempts_number.zero?
    raise
  end
end

# good: ジョブを短期間リトライし、リトライ回数に上限がある
class SomeJob < ApplicationJob
  sidekiq_options retry: 3
  sidekiq_retry_in do |count, exception|
    case exception
    when ThirdParty::Api::Errors::TooManyRequestsError
      count + 1 # i.e. 1s, 2s, 3s
    end
  end

  def perform(user)
    ThirdParty::Api::User.renew(user.external_id)
  end
end

# good: ジョブ内でのAPI利用が細かく制御されている
class SomeJob < ApplicationJob
  def perform(user)
    LIMITER.within_limit do
      ThirdParty::Api::User.renew(user.external_id)
    end
  end
end

# config/initializers/sidekiq.rb
Sidekiq::Limiter.configure do |config|
  config.errors << ThirdParty::Api::Errors::TooManyRequestsError
end

🔗 インフラ

🔗 ワンコアあたりワンプロセス

マルチコアのコンピュータでは、コアを使い切るために必要に応じてSidekiqプロセスをできるだけ多数実行すること。ひとつのSidekiqプロセスはCPUコアを1つしか使いません。要するに、コアに空きがある限りプロセスをなるべく多数実行することです。

🔗 Redisのメモリ制約

Redisのデータベースサイズは、サーバーのメモリによって制限されます。maxmemoryを明示的に設定することを好む人もいますが、noevictionポリシーと組み合わせたときにジョブのスケジューリングでエラーが発生する可能性があります。

🔗 死んだジョブ

死んだジョブをそのままにしないこと。死んだジョブで拡張バックトレースが有効になっていると、死んだジョブ1個だけでデータベース内で20KBを専有することがあります。

死んだジョブの根本原因を修正してジョブを再実行するか、でなければジョブを削除すること。

🔗 多すぎる引数

ジョブに引数を渡しすぎないこと。

# bad
SomeJob.perform_later(user_name, user_status, user_url, user_info: huge_json)

# good
SomeJob.perform_later(user, user_url)

🔗 ジョブの大群

数百個〜数千個におよびジョブを同一時刻に同時起動するスケジュールにしないこと。パラメータなしのジョブ1個ですら0.5KBを消費します。ジョブに引数を渡したときの正確なフットプリントをジョブごとに測定すること。

🔗 監視

サーバーやストアでのメトリクスの移り変わりを監視すること。メトリクスを正しく設定することで、ジョブ処理のスループットを改善する正しい答えを得られます。

🔗 商用機能

スケールの規模によっては、有料の商用機能が採算に見合うこともあります。

こうした商用機能には、サードパーティ製アドオンの形で利用できるものもありますが、その多くは信頼性に疑問が残ります。

🔗 Sidekiq Batchesを利用する

ひとつのタスクに関連するジョブがいくつもある場合はSidekiq Batchesでグループ化しましょう。Sidekiq Batchesのjobsメソッドはアトミックなので、すべてのジョブが「オールオアナッシング」形式でまとめてスケジューリングされます。

# bad
class BackfillMissingDataJob < ApplicationJob
  def self.run_batch
    Model.where(attribute: nil).find_each do |model|
      perform_later(model)
    end
  end

  def perform(model)
    # do the job
  end
end

# good
class BackfillMissingDataJob < ApplicationJob
  def self.run_batch
    batch = Sidekiq::Batch.new
    batch.description = 'Backfill missing data'
    batch.on(:success, BackfillComplete, to: SysAdmin.email)
    batch.jobs do
      Model.where(attribute: nil).find_each do |model|
        perform_later(model)
      end
    end
  end

  def perform(model)
    # ジョブを実行する
  end
end

🔗 ジョブのセルフスケジューリング

実行時間の長いジョブでセルフスケジューリング機能を使うのは避けること。Sidekiq Batchesで負荷を分割するのが望ましい方法です。

# bad
class BackfillMissingDataJob < ApplicationJob
  SIZE = 20
  def perform(offset = 0)
    models = Model.where(attribute: nil)
      .order(:id).offset(offset).limit(SIZE)
    return if models.empty?

    models.each do |model|
      model.update!(attribute: for(model))
    end
    self.class.perform_later(offset + SIZE)
  end
end

# good
class BackfillMissingDataJob < ApplicationJob
  def self.run_batch
    Sidekiq::Batch.new.jobs do
      Model.where(attribute: nil)
        .find_in_batches(20) do |models|
        BackfillMissingDataJob.perform_later(models)
      end
    end
  end

  def perform(models)
    models.each do |model|
      model.update!(attribute: for(model))
    end
  end
end

🔗 API操作に上限を設定する

ほとんどのサードパーティAPIには利用頻度の上限が設定されており、一定期間内の呼び出し回数がこの上限を超えると失敗します。こうした外部呼び出しを行うジョブでは呼び出し頻度に上限を設定すること。

実行されるジョブの数に決して依存しないようにすること。多数のジョブを実行時刻を変えてスケジューリングしたとしても、ジョブ処理の詰まりなどによって多数のジョブが一気に実行される可能性があります。SidekiqのEnterprise Rate Limiting機能を用いること。同機能の「コンカレント」「バケット」「ウィンドウ」戦略は、特定のAPIに対するほとんどの上限設定で通用します。

# bad
class UpdateExternalDataJob < ApplicationJob
  def perform(user)
    new_attribute = ThirdParty::Api.get_attribute(user.external_id)
    user.update!(attribute: new_attribute)
  end
end

User.where.not(external_id: nil)
  .find_in_batches.with_index do |group_number, users|
  users.each do |user|
    UpdateExternalDataJob
      .set(wait: group_number.minutes)
      .perform_later(users)
    end
end

# good
class UpdateExternalDataJob < ApplicationJob
  LIMITER = Sidekiq::Limiter.window('third-party-attribute-update', 20, :minute, wait_timeout: 0)

  def perform(user)
    LIMITER.within_limit do
      new_attribute = ThirdParty::Api.get_attribute(user.external_id)
      user.update!(attribute: new_attribute)
    end
  end
end

# アプリケーションのコード
User.where.not(external_id: nil).find_each do |user|
  UpdateExternalDataJob.perform_later(user)
end

# config/initializers/sidekiq.rb
Sidekiq::Limiter.configure do |config|
  config.errors << ThirdParty::Api::Errors::TooManyRequestsError
end

🔗 Sidekiqのデフォルト「limited backoff」

Sidekiqのデフォルト「limited backoff」に依存しないこと。この機能を使うと、ジョブが5分以内にリスケされます。

DEFAULT_BACKOFF = ->(limiter, job) do
  (300 * job['overrated']) + rand(300) + 1
end

この機能は、制約が速やかに解除される場合や、制約が数時間維持される場合には不向きです。以下のようにリミッターで設定するのが基本です。

Sidekiq::Limiter.configure do |config|
  config.backoff = ->(limiter, job) do
    case limiter.name
    when 'daily-third-party-api-limit'
      12.hours
    else
      (300 * job['overrated']) + rand(300) + 1 # fallback to default
    end
  end
end

リミッターの比較の仕組みを理解しておくこと。リミッター同士はオブジェクトではなく名前で比較すること。

Sidekiq::Limiter.bucket('custom-limiter', 1, :day) == Sidekiq::Limiter.bucket('custom-limiter', 1, :day) # => false

🔗 リミッターを再利用する

リミッターは起動中に1度作成しておき、それらを再利用すること。リミッターの設計はスレッドセーフで、かつ共有可能です。

リミッターごとにデフォルトでRedis内で114バイトを専有し、デフォルトのTTLは3か月です。共有されていないリミッターを使ってひと月あたり百万件のジョブを動かすと、Redis内で常に300MBを消費します。

# bad: ジョブ呼び出しのたびにリミッターが再作成される
class SomeJob < ApplicationJob
  def perform(...)
    limiter = Sidekiq::Limiter.concurrent('erp', 50, wait_timeout: 0, lock_timeout: 30)
    limiter.within_limit do
      # call ERP
    end
  end
end

# good
class SomeJob < ApplicationJob
  ERP_LIMIT = Sidekiq::Limiter.concurrent('erp', 50, wait_timeout: 0, lock_timeout: 30)

  def perform(...)
    ERP_LIMIT.within_limit do
      # call ERP
    end
  end
end

# 容認可能: 例外は「リミッターが特定目的だけに使われる場合」と
# 「リミッター名の中で識別キーとして使われる場合」
class SomeJob < ApplicationJob
  def perform(user)
    # Rate limiting is per user account
    user_throttle = Sidekiq::Limiter.bucket("stripe-#{user.id}", 30, :second, wait_timeout: 0)
    user_throttle.within_limit do
      # call stripe with user's account creds
    end
  end
end

🔗 リミッターのオプション

リミッターオプションの利用方法を誤ると、振る舞いがおかしくなることがあります。

🔗 wait_timeout

wait_timeoutは、ゼロまたは十分小さな値に設定すること。さもないと、実行待ちのジョブがキューに入っていたとしてもアイドル状態のワーカーが量産されます。

backoff設定を必ずチェックして、ジョブをリトライするタイミングを注意深く選んでおくこと。

🔗 コンカレントリミッターのlock_timeout

lock_timeoutは、ジョブ実行時間よりも長くすること。さもないと、ロックの解放が早くなりすぎて予想を超える数のコンカレントジョブが実行されます。

🔗 グローバルなリミッターミドルウェア

Sidekiq::Limiter::OverLimit例外はジョブによってrescueされる可能性があります(ローカルで定義されたリミッターからジョブ自身を廃棄する目的で)。グローバルなリミッターミドルウェアとローカルのジョブリミッターの干渉を避けるには、Sidekiq::Limiter::OverLimit例外をミドルウェアでラップします。

# ミドルウェア
class SaturationLimiter
  SaturationOverLimit = Class.new(StandardError)

  def self.wrapper(job, block)
    LIMITER.within_limit { block.call }
  rescue Sidekiq::Limiter::OverLimit => e
    limiter_name = e.limiter.name
    # ジョブレベルで定義されたリミッターからの
    # 「制限オーバー」例外の場合は再度raiseする
    raise unless limiter_name == LIMITER.name

    # Sidekiq::Limiterがジョブを後で実行するためにリスケするための
    # カスタム例外を使うこと
    # ただしジョブレベルで定義されたリミッターと衝突しないようにすること
    raise SaturationOverLimit, limiter_name
  end
end

# config/initializers/active_job.rb
ActiveJob::Base.around_perform(&SidekiqLimiter.method(:wrapper))

🔗 サードパーティサービスでOverLimitを無視する

Sidekiq::Limiter::OverLimitは内部メカニズムなので、トリガーされたときにこの例外を通知する意味はありません。

# config/initializers/rollbar.rb
Rollbar.configure do |config|
  config.exception_level_filters.merge!('Sidekiq::Limiter::OverLimit' => 'ignore')
end
# config/newrelic.yml
production:
  error_collector:
    enabled: true
    ignore_errors: "Sidekiq::Limiter::OverLimit"

🔗 ローリング再起動

SidekiqのEnterprise Rolling Restartsを使うこと。ローリング再起動することで、デプロイがダウンタイムの影響を受けずに済みますし、アトミックでないジョブや冪等でないジョブがデプロイ時に2回以上実行されることを防げます。

警告
Capistranoスタイルのデプロイでは、コードや依存関係のストール防止のため必ず--reexec-as--drop-env-var BUNDLE_GEMFILEオプションを指定すること。

🔗 テスト

🔗 perform

job.performjob_class.new.performは、Active Jobのシリアライズ/デシリアライズのステージをバイパスしてしまうので使わないこと。使うならjob_class.perform_nowにすること。暗黙で設定されるsubject.performは使わないことが推奨されます(既に正しく言及されているように、クラスではなくジョブインスタンスでのみ使えます)。

# bad: 暗黙で定義された`subject`で`perform`メソッドが直接呼ばれてしまう
RSpec.describe SomeJob do
  # 暗黙で定義された`subject`は`SomeJob.new`
  it 'updates user status' do
    expect { subject.perform(user) }.to change { user.status }.to(:updated) }
  end
end

# bad: `perform`メソッドがジョブインスタンスで直接呼ばれてしまう
RSpec.describe SomeJob do
  it 'updates user status' do
    expect { SomeJob.new.perform(user) }.to change { user.status }.to(:updated) }
  end
end

# good
RSpec.describe SomeJob do
  it 'updates user status' do
    expect { SomeJob.perform_now(user) }.to change { user.status }.to(:updated) }
  end
end

🔗 perform_later

ジョブのテストではperform_laterよりもperform_nowを優先的に使うこと。perform_laterはRedisと無関係です。

# bad: Redisとの不要な往復が発生する
RSpec.describe SomeJob do
  it 'updates user status' do
    expect do
      SomeJob.perform_later(user)
      perform_scheduled_jobs
    end.to change { user.status }.to(:updated) }
  end
end

# good
RSpec.describe SomeJob do
  it 'updates user status' do
    expect { SomeJob.perform_now(user) }.to change { user.status }.to(:updated) }
  end
end

🔗 本ガイド作成の経緯

本ガイドは、ActiveJobでSidekiqを用いるときのベストプラクティスを企業内部向けにリスト化したものとして誕生し、別のバックグラウンドジョブ処理ツールからSidekiqに移行したときの、おびただしいコードレビューから発言を集めて編集しました。初期段階では、Phil Pirozhkovが同僚の力添えを得て作成し、Toptalがスポンサーになりました。

🔗 本ガイドへの貢献について

本ガイドの作成は現在も進行中です。皆さんにガイドを改善いただけると、Rubyコミュニティにとって大きな(そしてシンプルな)助けとなります!

本ガイドに書かれている内容は、石に刻んだような不動のものではありません。私たちは、バックグラウンドジョブを用いる業務のベストプラクティスを本ガイドに集約することに関心のあるすべての人々とともに力を合わせてガイドを良くしていきたいと望んでいます。目標は、Rubyコミュニティ全体にとって有益な情報源を作成することです。

issueのオープンや改善のためのプルリクエスト送信はお気軽にどうぞ。皆さまのご協力にあらかじめ感謝申し上げます。

🔗 貢献方法

貢献は難しくありません。以下のガイドラインに沿っていただくだけで結構です。

🔗 ライセンス

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License

🔗 本ガイドを広めましょう

コミュニティドリブンのスタイルガイドは、その存在がコミュニティに知られなければほとんど役に立ちません。本ガイドをツイートして、お友だちや同僚との共有をお願いします。本ガイドへのどんなコメント、提案、ご意見もガイドをささやかに改善するのに役立ちます。皆さんも、可能な限りベストなガイドがある方がよいですよね?

関連記事

Rails 5.1〜7.0: ‘form_with’ APIドキュメント(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。