diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index eec400590a9..2d52736621e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -109,6 +109,7 @@ module ActiveRecord def initialize; end def state; end def closed?; true; end + alias_method :blank?, :closed? def open?; false; end def joinable?; false; end def add_record(record, _ = true); end @@ -272,8 +273,6 @@ module ActiveRecord def full_rollback?; true; end def joinable?; @joinable; end - def closed?; false; end - def open?; !closed?; end private def unique_records diff --git a/activerecord/lib/active_record/transaction.rb b/activerecord/lib/active_record/transaction.rb index c9bb96fa8d3..baa098fb343 100644 --- a/activerecord/lib/active_record/transaction.rb +++ b/activerecord/lib/active_record/transaction.rb @@ -3,6 +3,46 @@ require "active_support/core_ext/digest" module ActiveRecord + # This abstract class specifies the interface to interact with the current transaction state. + # + # Any other methods not specified here are considered to be private interfaces. + # + # == Callbacks + # + # After updating the database state, you may sometimes need to perform some extra work, or reflect these + # changes in a remote system like clearing or updating a cache: + # + # def publish_article(article) + # article.update!(published: true) + # NotificationService.article_published(article) + # end + # + # The above code works but has one important flaw, which is that it no longer works properly if called inside + # a transaction, as it will interact with the remote system before the changes are persisted: + # + # Article.transaction do + # article = create_article(article) + # publish_article(article) + # end + # + # The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible + # with transactions: + # + # def publish_article(article) + # article.update!(published: true) + # Article.current_transaction.after_commit do + # NotificationService.article_published(article) + # end + # end + # + # In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked + # after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked + # immediately. + # + # == Caveats + # + # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction + # won't be rolled back. Relying solely on these to synchronize state between multiple systems may lead to consistency issues. class Transaction class Callback # :nodoc: def initialize(event, callback) @@ -35,6 +75,8 @@ module ActiveRecord # If the current transaction has a parent transaction, the callback is transferred 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. + # + # If the callback raises an error, the transaction is rolled back. def before_commit(&block) (@callbacks ||= []) << Callback.new(:before_commit, block) end @@ -46,6 +88,8 @@ module ActiveRecord # If the current transaction has a parent transaction, the callback is transferred 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. + # + # If the callback raises an error, the transaction remains committed. def after_commit(&block) (@callbacks ||= []) << Callback.new(:after_commit, block) end @@ -63,13 +107,24 @@ module ActiveRecord (@callbacks ||= []) << Callback.new(:after_rollback, block) end + # Returns true if a transaction was started. + def open? + true + end + alias_method :blank?, :open? + + # Returns true if no transaction is currently active. + def closed? + false + end + # Returns a UUID for this transaction. def uuid @uuid ||= Digest::UUID.uuid_v4 end protected - def append_callbacks(callbacks) + def append_callbacks(callbacks) # :nodoc: (@callbacks ||= []).concat(callbacks) end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 63aeecad026..bc4b02226d3 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -235,7 +235,13 @@ module ActiveRecord end end - # Returns the current transaction. See ActiveRecord::Transactions API docs. + # Returns a representation of the current transaction state, + # which can be a top level transaction, a savepoint, or the absence of a transaction. + # + # An object is always returned, whether or not a transaction is currently active. + # To check if a transaction was opened, use current_transaction.open?. + # + # See the ActiveRecord::Transaction documentation for detailed behavior. def current_transaction connection_pool.active_connection&.current_transaction || ConnectionAdapters::TransactionManager::NULL_TRANSACTION end