Skip to content

Commit 41d867d

Browse files
authored
Merge pull request rails#51525 from Shopify/aj-after-commit-return-value
Fix `ActiveJob::EnqueueAfterTransactionCommit` API
2 parents 61d78da + afa0198 commit 41d867d

File tree

7 files changed

+185
-24
lines changed

7 files changed

+185
-24
lines changed

activejob/lib/active_job/enqueue_after_transaction_commit.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ module EnqueueAfterTransactionCommit # :nodoc:
1616
# - `:never` forces the job to be queued immediately.
1717
# - `:default` lets the queue adapter define the behavior (recommended).
1818
class_attribute :enqueue_after_transaction_commit, instance_accessor: false, instance_predicate: false, default: :never
19+
end
1920

20-
around_enqueue do |job, block|
21-
after_transaction = case job.class.enqueue_after_transaction_commit
21+
private
22+
def raw_enqueue
23+
after_transaction = case self.class.enqueue_after_transaction_commit
2224
when :always
2325
true
2426
when :never
@@ -28,11 +30,15 @@ module EnqueueAfterTransactionCommit # :nodoc:
2830
end
2931

3032
if after_transaction
31-
ActiveRecord.after_all_transactions_commit(&block)
33+
self.successfully_enqueued = true
34+
ActiveRecord.after_all_transactions_commit do
35+
self.successfully_enqueued = false
36+
super
37+
end
38+
self
3239
else
33-
block.call
40+
super
3441
end
3542
end
36-
end
3743
end
3844
end

activejob/lib/active_job/enqueuing.rb

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,17 @@ module ClassMethods
5050
# custom serializers.
5151
#
5252
# Returns an instance of the job class queued with arguments available in
53-
# Job#arguments or false if the enqueue did not succeed.
53+
# Job#arguments or +false+ if the enqueue did not succeed.
5454
#
5555
# After the attempted enqueue, the job will be yielded to an optional block.
5656
#
5757
# If Active Job is used conjointly with Active Record, and #perform_later is called
5858
# inside an Active Record transaction, then the enqueue is implicitly deferred to after
59-
# the transaction is committed, or droped if it's rolled back. This behavior can
60-
# be changed on a per job basis:
59+
# the transaction is committed, or droped if it's rolled back. In such case #perform_later
60+
# will return the job instance like if it was successfully enqueued, but will still return
61+
# +false+ if a callback prevented the job from being enqueued.
62+
#
63+
# This behavior can be changed on a per job basis:
6164
#
6265
# class NotificationJob < ApplicationJob
6366
# self.enqueue_after_transaction_commit = false
@@ -98,6 +101,18 @@ def enqueue(options = {})
98101
self.successfully_enqueued = false
99102

100103
run_callbacks :enqueue do
104+
raw_enqueue
105+
end
106+
107+
if successfully_enqueued?
108+
self
109+
else
110+
false
111+
end
112+
end
113+
114+
private
115+
def raw_enqueue
101116
if scheduled_at
102117
queue_adapter.enqueue_at self, scheduled_at.to_f
103118
else
@@ -108,12 +123,5 @@ def enqueue(options = {})
108123
rescue EnqueueError => e
109124
self.enqueue_error = e
110125
end
111-
112-
if successfully_enqueued?
113-
self
114-
else
115-
false
116-
end
117-
end
118126
end
119127
end
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# frozen_string_literal: true
2+
3+
require "helper"
4+
require "jobs/enqueue_error_job"
5+
6+
class EnqueueAfterTransactionCommitTest < ActiveSupport::TestCase
7+
class FakeActiveRecord
8+
attr_reader :calls
9+
10+
def initialize(should_yield = true)
11+
@calls = 0
12+
@yield = should_yield
13+
@callbacks = []
14+
end
15+
16+
def after_all_transactions_commit(&block)
17+
@calls += 1
18+
if @yield
19+
yield
20+
else
21+
@callbacks << block
22+
end
23+
end
24+
25+
def run_after_commit_callbacks
26+
callbacks, @callbacks = @callbacks, []
27+
callbacks.each(&:call)
28+
end
29+
end
30+
31+
class EnqueueAfterCommitJob < ActiveJob::Base
32+
self.enqueue_after_transaction_commit = :always
33+
34+
def perform
35+
# noop
36+
end
37+
end
38+
39+
class ErrorEnqueueAfterCommitJob < EnqueueErrorJob
40+
class EnqueueErrorAdapter
41+
def enqueue_after_transaction_commit?
42+
true
43+
end
44+
45+
def enqueue(...)
46+
raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
47+
end
48+
49+
def enqueue_at(...)
50+
raise ActiveJob::EnqueueError, "There was an error enqueuing the job"
51+
end
52+
end
53+
54+
self.queue_adapter = EnqueueErrorAdapter.new
55+
self.enqueue_after_transaction_commit = :always
56+
57+
def perform
58+
# noop
59+
end
60+
end
61+
62+
test "#perform_later wait for transactions to complete before enqueuing the job" do
63+
fake_active_record = FakeActiveRecord.new
64+
stub_const(Object, :ActiveRecord, fake_active_record, exists: false) do
65+
assert_difference -> { fake_active_record.calls }, +1 do
66+
EnqueueAfterCommitJob.perform_later
67+
end
68+
end
69+
end
70+
71+
test "#perform_later returns the Job instance even if it's delayed by `after_all_transactions_commit`" do
72+
fake_active_record = FakeActiveRecord.new(false)
73+
stub_const(Object, :ActiveRecord, fake_active_record, exists: false) do
74+
job = EnqueueAfterCommitJob.perform_later
75+
assert_instance_of EnqueueAfterCommitJob, job
76+
assert_predicate job, :successfully_enqueued?
77+
end
78+
end
79+
80+
test "#perform_later yields the enqueued Job instance even if it's delayed by `after_all_transactions_commit`" do
81+
fake_active_record = FakeActiveRecord.new(false)
82+
stub_const(Object, :ActiveRecord, fake_active_record, exists: false) do
83+
called = false
84+
job = EnqueueAfterCommitJob.perform_later do |yielded_job|
85+
called = true
86+
assert_instance_of EnqueueAfterCommitJob, yielded_job
87+
end
88+
assert called, "#perform_later yielded the job"
89+
assert_instance_of EnqueueAfterCommitJob, job
90+
assert_predicate job, :successfully_enqueued?
91+
end
92+
end
93+
94+
test "#perform_later assumes successful enqueue, but update status later" do
95+
fake_active_record = FakeActiveRecord.new(false)
96+
stub_const(Object, :ActiveRecord, fake_active_record, exists: false) do
97+
job = ErrorEnqueueAfterCommitJob.perform_later
98+
assert_instance_of ErrorEnqueueAfterCommitJob, job
99+
assert_predicate job, :successfully_enqueued?
100+
101+
fake_active_record.run_after_commit_callbacks
102+
assert_not_predicate job, :successfully_enqueued?
103+
end
104+
end
105+
end

activejob/test/helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ def adapter_is?(*adapter_class_symbols)
2323
end
2424

2525
require_relative "../../tools/test_common"
26+
27+
ActiveJob::Base.include(ActiveJob::EnqueueAfterTransactionCommit)

activesupport/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* `stub_const` now accepts a `exists: false` parameter to allow stubbing missing constants.
2+
3+
*Jean Boussier*
4+
15
* Make ActiveSupport::BacktraceCleaner copy filters and silencers on dup and clone
26

37
Previously the copy would still share the internal silencers and filters array,

activesupport/lib/active_support/testing/constant_stubbing.rb

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,39 @@ module ConstantStubbing
1515
# Using this method rather than forcing <tt>World::List::Import::LARGE_IMPORT_THRESHOLD = 5000</tt> prevents
1616
# warnings from being thrown, and ensures that the old value is returned after the test has completed.
1717
#
18+
# If the constant doesn't already exists, but you need it set for the duration of the block
19+
# you can do so by passing `exists: false`.
20+
#
21+
# stub_const(object, :SOME_CONST, 1, exists: false) do
22+
# assert_equal 1, SOME_CONST
23+
# end
24+
#
1825
# Note: Stubbing a const will stub it across all threads. So if you have concurrent threads
1926
# (like separate test suites running in parallel) that all depend on the same constant, it's possible
2027
# divergent stubbing will trample on each other.
21-
def stub_const(mod, constant, new_value)
22-
old_value = mod.const_get(constant, false)
23-
mod.send(:remove_const, constant)
24-
mod.const_set(constant, new_value)
25-
yield
26-
ensure
27-
mod.send(:remove_const, constant)
28-
mod.const_set(constant, old_value)
28+
def stub_const(mod, constant, new_value, exists: true)
29+
if exists
30+
begin
31+
old_value = mod.const_get(constant, false)
32+
mod.send(:remove_const, constant)
33+
mod.const_set(constant, new_value)
34+
yield
35+
ensure
36+
mod.send(:remove_const, constant)
37+
mod.const_set(constant, old_value)
38+
end
39+
else
40+
if mod.const_defined?(constant)
41+
raise NameError, "already defined constant #{constant} in #{mod.name}"
42+
end
43+
44+
begin
45+
mod.const_set(constant, new_value)
46+
yield
47+
ensure
48+
mod.send(:remove_const, constant)
49+
end
50+
end
2951
end
3052
end
3153
end

activesupport/test/test_case_test.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ class TestConstStubbing < ActiveSupport::TestCase
580580
assert_equal 1, ConstStubbable::CONSTANT
581581
end
582582

583-
test "trying to stub a constant that does not exist in the receiver raises NameError" do
583+
test "stubbing a constant that does not exist in the receiver raises NameError" do
584584
assert_raises(NameError) do
585585
stub_const(ConstStubbable, :NOT_A_CONSTANT, 1) { }
586586
end
@@ -589,4 +589,18 @@ class TestConstStubbing < ActiveSupport::TestCase
589589
stub_const(SubclassOfConstStubbable, :CONSTANT, 1) { }
590590
end
591591
end
592+
593+
test "stubbing a constant that does not exist can be done with `exists: false`" do
594+
stub_const(ConstStubbable, :NOT_A_CONSTANT, 1, exists: false) do
595+
assert_equal 1, ConstStubbable::NOT_A_CONSTANT
596+
end
597+
598+
assert_raises(NameError) do
599+
ConstStubbable::NOT_A_CONSTANT
600+
end
601+
602+
assert_raises(NameError) do
603+
stub_const(Object, :ConstStubbable, 1, exists: false)
604+
end
605+
end
592606
end

0 commit comments

Comments
 (0)