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?
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:
Now, when you run TestJob.perform_now, you should see :baz printed.