kakudooo docs

Active Job のジョブから別のジョブを呼び出す

Rails で Web アプリケーションを開発していると Active Job を使って、非同期処理を実装することがあります。 非同期処理の担う責務が単純なうちは単一のジョブで処理を完結していても特に困らないですが、複数の外部 API を組み合わせる形でジョブが実装されるようになると、一つのジョブが担う責務が大きくなってきます。

不具合時の復旧やジョブを構成するコードの責務を適切に分離する目的で、単一のジョブを責務に応じて分割し、親ジョブから子ジョブを登録するような設計を検討したくなります。 また、設計において親子で特定のパラメータを受け渡し、それに依存する Workflow のような形をとるものを検討する場合もあるかと思います。

今回は Active Job を使った非同期処理におけるジョブの分割と、分割において考慮すべき点について整理します。

また、サンプルコードでは、バックエンドワーカーとしてdelayed_jobの使用を想定しています。

ジョブの分割

ジョブ分割の例です。問い合わせが合った場合に CRM にユーザー情報を作成し、何かしらのチャットシステムに通知を送ることにします。 単純すぎて一つのジョブでまとめて処理を行っても問題はなさそうなものですが、少しでも今回やりたいことが伝わればと思います。

before

CreateInquiryToCrmJob

を行っています。

class CreateInquiryToCrmJob < ApplicationJob
  queue_as :default

  self.log_arguments = false

  def perform(name, email)
    crm_clinet = CRM::Client.new
    user = crm_client.fetch_user(email)
    user = crm_client.create_user(name, email) if user.nil?

    chat_client = Chat::Client.new('channel_id')
    chat_client.send_message("#{user.name}様から問い合わせがありました。")
  end
end

after

CreateInquiryToCrmJob

ChatNotificationJob

を行うようにしました。

これによって、CRM サービスへの操作と通知の責務を別々のジョブとして分けることができ

よりロジックが複雑な場合は、上記のような恩恵を得ることができるのではないかと思います。

class ChatNotificationJob < ApplicationJob
  queue_as :default

  self.log_arguments = false

  def perform(user_name)
    chat_client = Chat::Client.new('channel_id')
    chat_client.send_message("#{user_name}様から問い合わせがありました。")
  end
end

class CreateInquiryToCrmJob < ApplicationJob
  queue_as :default

  self.log_arguments = false

  def perform(name, email)
    crm_clinet = CRM::Client.new
    user = crm_client.fetch_user(email)
    user = crm_client.create_user(name, email) if user.nil?

    ChatNotificationJob.perform_later(user.name)
  end
end

実際に今回のようにジョブから別のジョブを作成することがあるのか?そのような事例はあるのか?という疑問についてですが、Rails を使って実装されているいくつかの OSS でも、ジョブから別のジョブを呼び出すような実装がありそうでした。

以下は、gitlabhqgumroadの該当コードです。

ジョブを分割する時の注意点

Active Job は非同期処理の実装におけるジョブの登録と実行のハンドリングにのみに責任を持ちます。

Active Job is a framework in Rails designed for declaring background jobs and executing them on a queuing backend

https://guides.rubyonrails.org/active_job_basics.html

つまり、対象のジョブ同士をどのように制御するか(Workflow)については責任の範囲外です。 これから自分が設計・実装するジョブが Workflow 的な性質を持つ or 持たないを考え、必要に応じて Workflow として処理するための設計やそのための機能がバックエンドワーカーに備わっているか?について検討する必要があります。

単なるジョブ

Workflow 的な性質を持たない単なるジョブの呼び出しです。今回扱いたかったテーマはこちらに該当します。 特定の外部 API との連携を行い、その結果をもってメールやチャットツールにメッセージを送信したいような場合については、ジョブ同士の制御についてのロジックを持つ必要はなく、ジョブキューの実装やバックエンドワーカーの実装について特段検討が必要になることはないです。

Workflow

ジョブを Workflow として処理するための設計については、以下のスライドにとてもわかりやすく検討すべき事項がまとまっています。

https://speakerdeck.com/mokuo/async-architecture-patterns?slide=12

などの項目について、検討する必要があります。

また、Workflow 的な非同期処理についてはEnterprise Integration Patterns (EIP) 観点でアーキテクチャの分析ができそうなので、また別の記事として書こうかと思います。

まとめ

Active Job を使用した非同期処理において、ジョブの分割と分割する際に考慮すべき点についてみてきました。

参考