acts_as_tenant icon indicating copy to clipboard operation
acts_as_tenant copied to clipboard

current_tenant missing while using with GoodJob and CurrentAttribute

Open EB-Plum opened this issue 9 months ago • 0 comments

Sorry, this might not help with identifying the root cause. Just wanted to share some ad-hoc workarounds I implemented for this bug(?). This happens when I call perform_later on another job from within a job.

I don't fully understand the interactions between ActiveJob, GoodJob, CurrentAttributes, and ActsAsTenant::ActiveJobExtensions.

From what I understand, when you spawn a new thread, ActsAsTenant.current_tenant is not preserved. Sometimes current attributes are lost during the serialize call, even though I assumed serialization happens during the enqueue process, and retrying jobs also causes context loss.

So I suspect that GoodJob runs ActiveJob in new threads, which would explain the context loss we see when new threads are spawned. To work around this, I'm manually preserving the context by storing it as job attributes and ensuring it gets properly serialized and restored when the job runs. (I don't understand why this way it's preserved!)

I created these ad-hoc patches to maintain job context consistency. Note that this code is not fully tested. It may broke on perform_now.

# class ApplicationJob < ActiveJob::Base
#   include JobContext
#   ...
# end

module JobContext
  extend ActiveSupport::Concern

  included do
    attr_accessor :host_with_port
    attr_accessor :tenant

    prepend Prepends

    before_enqueue { |job| job.set_context }
    around_perform do |job, block|
      with_context do
        block.call
      end
    end

    def set_context
      self.host_with_port = CurrentContext.host_with_port
      self.tenant = ActsAsTenant.current_tenant
    end

    def with_context
      CurrentContext.with(host_with_port: host_with_port) do
        ActsAsTenant.with_tenant(tenant) do
          yield
        end
      end
    end
  end

  module Prepends
    def serialize
      super.merge("host_with_port" => self.host_with_port)
          .merge("tenant" => self.tenant&.to_global_id&.to_s)
    end

    def deserialize(job_data)
      self.host_with_port = job_data["host_with_port"]
      self.tenant = job_data["tenant"].present? ? GlobalID::Locator.locate(job_data["tenant"]) : nil
      super(job_data)
    end
  end
end
# Not essential code, just showing what I was doing
class CurrentContext < ActiveSupport::CurrentAttributes
  attribute :host_with_port
  attribute :locale
  attribute :timezone

  def self.reset
    self.host_with_port = nil
    self.locale = nil
    self.timezone = nil
  end
end

EB-Plum avatar Jun 30 '25 05:06 EB-Plum