7
8

More than 5 years have passed since last update.

ApartmentとDelayedJobでマルチテナントJob

Last updated at Posted at 2015-09-03

公式では非推奨

Delayed::Job
Has been removed... See apartment-sidekiq for a better backgrounding experience

公式では上記のようにDelayedJob対応は削除され、sidekiqを推奨していますが
諸事情によりDelayedJobを採用した際のモンキーパッチ対応です

sidekiqでDBConnectionPool系エラー

複数のテナントのJOBを同時に実行すると発生
リトライ時や逐次実行など、他テナントJOBが動いてなければ成功するので
sidekiqはマルチスレッドだがテナント切替のestablish_connectionが
スレッドセーフではないことが原因か?

環境

Ruby v2.2.2
Rails v4.2.3
gem apartment v1.0.2
gem delayed_job_active_record v4.0.3

Apartment

テナント毎のDBにjobデータが分散するとDelayedJobが取得できないので
デフォルトDBに集約する設定を追加する

config/initializers/apartment.rb
Apartment.configure do |config|
  config.excluded_models = %w{ Delayed::Backend::ActiveRecord::Job }
end

DelayedJob

全テナントのjobデータが入るため、区別のためテナントカラムを増やす

db/migrate/***_create_delayed_jobs.rb
class CreateDelayedJobs < ActiveRecord::Migration
  def self.up
    create_table :delayed_jobs, force: true do |t|
      t.string :tenant,                null: false # マルチテナント対応による拡張
      # (略)delayed_jobの各カラム
    end
  end
end

新実装方法

旧実装と考え方は変わっていないが
テナント情報の保存とJob実行時に接続を切り替える処理をそれぞれ拡張

config/initializers/delayed_job.rb
Delayed::Worker.prepend(Delayed::Worker::Multitenancy)
Delayed::Backend::ActiveRecord::Job.prepend(Delayed::Backend::ActiveRecord::Multitenancy)

実際のRun処理のみを拡張する

lib/autoload/delayed/worker/multitenancy.rb
module Delayed
  class Worker
    module Multitenancy
      def run(job)
        in_tenant(job.tenant) do
          super
        end
      end

      private

      def in_tenant(tenant)
        Apartment::Tenant.switch(tenant) do
          yield
        end
      end
    end
  end
end

分割しただけで旧実装と変わらず

lib/autoload/delayed/backend/active_record/multitenancy.rb
module Delayed
  module Backend
    module ActiveRecord
      module Multitenancy
        def self.prepended(mod)
          mod.class_eval do
            after_initialize :set_tenant, if: :new_record?
          end
        end

        private

        def set_tenant
          self.tenant = Apartment::Tenant.current
        end
      end
    end
  end
end

結果

無事に text_after_commit を入れたrspecで成功

旧拡張方法(非推奨・参考記録)

テナント情報の保存とJob実行時に接続を切り替える処理を拡張

config/initializers/delayed_job.rb
class Delayed::Backend::ActiveRecord::Job
  after_initialize :bind_tenant, if: :new_record?

  # Jobオブジェクト生成
  def payload_object_with_tenant
    tenanted_job do
      payload_object_without_tenant
    end
  end
  alias_method_chain :payload_object, :tenant

  # Job実行
  def invoke_job_with_tenant
    tenanted_job do
      invoke_job_without_tenant
    end
  end
  alias_method_chain :invoke_job, :tenant

  private

  def tenanted_job(&block)
    result = nil
    Apartment::Tenant.switch(self.tenant) do
      result = block.call
    end
    result
  end

  def bind_tenant
    self.tenant = Apartment::Tenant.current
  end
end

結果

job取得やjob実行で毎回コネクションを切り替えるため効率は良くない
gem text_after_commit を導入してrspecのafter_commitでjob化せずに実行するとエラー
実際の動作は問題無さそうだがtext_after_commitはRails5で取り込まれるので要改善

参考

7
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8