Skip to content

Commit 2514bde

Browse files
authored
Merge pull request rails#51968 from Shopify/current-transaction-doc
Clarify `current_transaction` behavior.
2 parents 8336aa7 + e104356 commit 2514bde

File tree

3 files changed

+64
-4
lines changed

3 files changed

+64
-4
lines changed

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class NullTransaction # :nodoc:
109109
def initialize; end
110110
def state; end
111111
def closed?; true; end
112+
alias_method :blank?, :closed?
112113
def open?; false; end
113114
def joinable?; false; end
114115
def add_record(record, _ = true); end
@@ -272,8 +273,6 @@ def commit_records
272273

273274
def full_rollback?; true; end
274275
def joinable?; @joinable; end
275-
def closed?; false; end
276-
def open?; !closed?; end
277276

278277
private
279278
def unique_records

activerecord/lib/active_record/transaction.rb

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,46 @@
33
require "active_support/core_ext/digest"
44

55
module ActiveRecord
6+
# This abstract class specifies the interface to interact with the current transaction state.
7+
#
8+
# Any other methods not specified here are considered to be private interfaces.
9+
#
10+
# == Callbacks
11+
#
12+
# After updating the database state, you may sometimes need to perform some extra work, or reflect these
13+
# changes in a remote system like clearing or updating a cache:
14+
#
15+
# def publish_article(article)
16+
# article.update!(published: true)
17+
# NotificationService.article_published(article)
18+
# end
19+
#
20+
# The above code works but has one important flaw, which is that it no longer works properly if called inside
21+
# a transaction, as it will interact with the remote system before the changes are persisted:
22+
#
23+
# Article.transaction do
24+
# article = create_article(article)
25+
# publish_article(article)
26+
# end
27+
#
28+
# The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible
29+
# with transactions:
30+
#
31+
# def publish_article(article)
32+
# article.update!(published: true)
33+
# Article.current_transaction.after_commit do
34+
# NotificationService.article_published(article)
35+
# end
36+
# end
37+
#
38+
# In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked
39+
# after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked
40+
# immediately.
41+
#
42+
# == Caveats
43+
#
44+
# When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
45+
# won't be rolled back. Relying solely on these to synchronize state between multiple systems may lead to consistency issues.
646
class Transaction
747
class Callback # :nodoc:
848
def initialize(event, callback)
@@ -35,6 +75,8 @@ def initialize # :nodoc:
3575
# If the current transaction has a parent transaction, the callback is transferred to
3676
# the parent when the current transaction commits, or dropped when the current transaction
3777
# is rolled back. This operation is repeated until the outermost transaction is reached.
78+
#
79+
# If the callback raises an error, the transaction is rolled back.
3880
def before_commit(&block)
3981
(@callbacks ||= []) << Callback.new(:before_commit, block)
4082
end
@@ -46,6 +88,8 @@ def before_commit(&block)
4688
# If the current transaction has a parent transaction, the callback is transferred to
4789
# the parent when the current transaction commits, or dropped when the current transaction
4890
# is rolled back. This operation is repeated until the outermost transaction is reached.
91+
#
92+
# If the callback raises an error, the transaction remains committed.
4993
def after_commit(&block)
5094
(@callbacks ||= []) << Callback.new(:after_commit, block)
5195
end
@@ -63,13 +107,24 @@ def after_rollback(&block)
63107
(@callbacks ||= []) << Callback.new(:after_rollback, block)
64108
end
65109

110+
# Returns true if a transaction was started.
111+
def open?
112+
true
113+
end
114+
alias_method :blank?, :open?
115+
116+
# Returns true if no transaction is currently active.
117+
def closed?
118+
false
119+
end
120+
66121
# Returns a UUID for this transaction.
67122
def uuid
68123
@uuid ||= Digest::UUID.uuid_v4
69124
end
70125

71126
protected
72-
def append_callbacks(callbacks)
127+
def append_callbacks(callbacks) # :nodoc:
73128
(@callbacks ||= []).concat(callbacks)
74129
end
75130
end

activerecord/lib/active_record/transactions.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,13 @@ def transaction(**options, &block)
235235
end
236236
end
237237

238-
# Returns the current transaction. See ActiveRecord::Transactions API docs.
238+
# Returns a representation of the current transaction state,
239+
# which can be a top level transaction, a savepoint, or the absence of a transaction.
240+
#
241+
# An object is always returned, whether or not a transaction is currently active.
242+
# To check if a transaction was opened, use <tt>current_transaction.open?</tt>.
243+
#
244+
# See the ActiveRecord::Transaction documentation for detailed behavior.
239245
def current_transaction
240246
connection_pool.active_connection&.current_transaction || ConnectionAdapters::TransactionManager::NULL_TRANSACTION
241247
end

0 commit comments

Comments
 (0)