Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow to register transaction callbacks outside of a record
Ref: #26103 Ref: #51426 A fairly common mistake with Rails is to enqueue a job from inside a transaction, and a record as argumemnt, which then lead to a RecordNotFound error when picked up by the queue. This is even one of the arguments advanced for job runners backed by the database such as `solid_queue`, `delayed_job` or `good_job`. But relying on this is undesirable iin my opinion as it makes the Active Job abstraction leaky, and if in the future you need to migrate to another backend or even just move the queue to a separate database, you may experience a lot of race conditions of the sort. But more generally, being able to defer work to after the current transaction has been a missing feature of Active Record. Right now the only way to do it is from a model callback, and this forces moving things in Active Record models that sometimes are better done elsewhere. Even as a self-proclaimed "service object skeptic", I often wanted this capability over the last decade, and I'm sure it got asked or desired by many more people. Also there's some 3rd party gems adding this capability using monkey patches. It's not a reason to upstream the capability, but it's a proof that there is demand for it. Implementation wise, this proof of concept shows that it's not really hard to implement, even with nested multi-db transactions support. Co-Authored-By: Cristian Bica <cristian.bica@gmail.com>
- Loading branch information
1 parent
eac95d5
commit c2df237
Showing
7 changed files
with
413 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveRecord | ||
class Transaction | ||
class Callback # :nodoc: | ||
def initialize(event, callback) | ||
@event = event | ||
@callback = callback | ||
end | ||
|
||
def before_commit | ||
@callback.call if @event == :before_commit | ||
end | ||
|
||
def after_commit | ||
@callback.call if @event == :after_commit | ||
end | ||
|
||
def after_rollback | ||
@callback.call if @event == :after_rollback | ||
end | ||
end | ||
|
||
def initialize # :nodoc: | ||
@callbacks = nil | ||
end | ||
|
||
# Registers a block to be called before the current transaction is fully committed. | ||
# | ||
# If there is no currently open transactions, the block is called immediately. | ||
# | ||
# If the current transaction has a parent transaction, the callback is transfered to | ||
# the parent when the current transaction commits, or dropped when the current transaction | ||
# is rolled back. This operation is repeated until the outermost transaction is reached. | ||
def before_commit(&block) | ||
(@callbacks ||= []) << Callback.new(:before_commit, block) | ||
end | ||
|
||
# Registers a block to be called after the current transaction is fully committed. | ||
# | ||
# If there is no currently open transactions, the block is called immediately. | ||
# | ||
# If the current transaction has a parent transaction, the callback is transfered to | ||
# the parent when the current transaction commits, or dropped when the current transaction | ||
# is rolled back. This operation is repeated until the outermost transaction is reached. | ||
def after_commit(&block) | ||
(@callbacks ||= []) << Callback.new(:after_commit, block) | ||
end | ||
|
||
# Registers a block to be called after the current transaction is rolled back. | ||
# | ||
# If there is no currently open transactions, the block is never called. | ||
# | ||
# If the current transaction is successfully committed but has a parent | ||
# transaction, the callback is automatically added to the parent transaction. | ||
# | ||
# If the entire chain of nested transactions are all successfully committed, | ||
# the block is never called. | ||
def after_rollback(&block) | ||
(@callbacks ||= []) << Callback.new(:after_rollback, block) | ||
end | ||
|
||
protected | ||
def append_callbacks(callbacks) | ||
(@callbacks ||= []).concat(callbacks) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.