Rails Concern when included in ActiveJob not as expected

210 Views Asked by At

I'm experiencing some strange behaviour when including a Concern into an ActiveJob class.

This is a scaled back version of the issue I'm having. I'm creating a concern, which uses a included block to set retry_on and pass it class names set in the individual jobs - I'm using puts my_method below to simplify the example.

module FooConcern
  extend ActiveSupport::Concern

  included do
    puts my_method
  end

  def self.my_method
    :foo
  end

  def my_method
    :bar
  end
end

class TestJob < ActiveJob::Base
  include FooConcern

  def self.my_method
    :baz
  end

  def my_method
    :biz
  end

  def perform; end
end

Inside the console, I am seeing the following when I try and run perform_now or perform_later, but as you can see, normal instantiation works as expected:

irb(main):001:0> TestJob.perform_now
Traceback (most recent call last):
        5: from (irb):1
        4: from app/jobs/test_job.rb:19:in `<main>'
        3: from app/jobs/test_job.rb:20:in `<class:TestJob>'
        2: from app/jobs/test_job.rb:20:in `include'
        1: from app/jobs/test_job.rb:7:in `block in <module:FooConcern>'
NameError (undefined local variable or method `my_method' for TestJob:Class)
Did you mean?  method

irb(main):002:0> TestJob.my_method
=> :baz

irb(main):003:0> TestJob.new.my_method
=> :biz

But, when I move the include FooConcern to the end of the class, it all works as I would expect:

class TestJob < ActiveJob::Base
  def self.my_method
    :baz
  end

  def my_method
    :biz
  end

  def perform; end

  include FooConcern
end
irb(main):001:0> TestJob.perform_now
baz

Performing TestJob (Job ID: 2239d1e8-d7cb-4a22-ae1d-cfc62bdb802a) from Async(default) enqueued at Performed TestJob (Job ID: 2239d1e8-d7cb-4a22-ae1d-cfc62bdb802a) from Async(default) in 0.08ms
=> nil

I also tried putting the methods inside the included block:

module FooConcern
  extend ActiveSupport::Concern

  included do
    puts my_method

    def self.my_method
      :foo
    end

    def my_method
      :bar
    end
  end
end

class TestJob < ActiveJob::Base
  include FooConcern

  # ...
end

With the same result:

irb(main):001:0> TestJob.perform_now
Traceback (most recent call last):
        5: from (irb):1
        4: from app/jobs/test_job.rb:19:in `<main>'
        3: from app/jobs/test_job.rb:20:in `<class:TestJob>'
        2: from app/jobs/test_job.rb:20:in `include'
        1: from app/jobs/test_job.rb:7:in `block in <module:FooConcern>'
NameError (undefined local variable or method `my_method' for TestJob:Class)

I tried prenpending the concern and using a prepended block, but saw the same results... when the concern is included at the top of the class it fails, at the bottom of the class it works.

Am I missing something? Is there a way to have access to the TestJob methods inside the included block inside the Concern?

1

There are 1 best solutions below

1
Alef ojeda de Oliveira On

This issue occurs because, when you include a concern at the top of a class definition, the concern's included method gets executed before the rest of the class definition is processed, while the rest of the methods of the concern are defined in the context of the including class. So, when you call my_method inside the included method, it first looks for the method in the concern, which doesn't have a definition for it. On the other hand, if you include the concern at the bottom of the class definition, the rest of the class definition gets processed first, so when you call my_method inside the concern, it looks for the method in the class, which does have a definition for it. A way to solve this is to pass the class that includes the concern as a parameter to the included method. You can then use that class to call its methods inside the concern. Here's an updated version of the code:

module FooConcern
  extend ActiveSupport::Concern

  included do |klass|
    puts klass.my_method
  end
end

class TestJob < ActiveJob::Base
  include FooConcern

  def self.my_method
    :baz
  end

  def my_method
    :biz
  end

  def perform; end
end

Now, when you run TestJob.perform_now, you should see :baz printed.