From f64a4134df1c50f88c4a6e111b839bb499407bd4 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Mon, 10 Jun 2024 17:30:37 +0200 Subject: [PATCH] Define the new start_transaction.active_record event --- .../abstract/transaction.rb | 4 +- .../cases/transaction_instrumentation_test.rb | 56 +++++++++++++++++++ .../source/active_support_instrumentation.md | 56 +++++++++++++++++-- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2d52736621..7b53dda416 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -91,7 +91,9 @@ def start raise InstrumentationAlreadyStartedError.new("Called start on an already started transaction") if @started @started = true - @payload = @base_payload.dup + ActiveSupport::Notifications.instrument("start_transaction.active_record", @base_payload) + + @payload = @base_payload.dup # We dup because the payload for a given event is mutated later to add the outcome. @handle = ActiveSupport::Notifications.instrumenter.build_handle("transaction.active_record", @payload) @handle.start end diff --git a/activerecord/test/cases/transaction_instrumentation_test.rb b/activerecord/test/cases/transaction_instrumentation_test.rb index a44fb6b3e7..b96bea584a 100644 --- a/activerecord/test/cases/transaction_instrumentation_test.rb +++ b/activerecord/test/cases/transaction_instrumentation_test.rb @@ -7,6 +7,62 @@ class TransactionInstrumentationTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :topics + def test_start_transaction_is_triggered_when_the_transaction_is_materialized + transactions = [] + subscriber = ActiveSupport::Notifications.subscribe("start_transaction.active_record") do |event| + assert event.payload[:connection] + transactions << event.payload[:transaction] + end + + Topic.transaction do |transaction| + assert_empty transactions # A transaction call, per se, does not trigger the event. + topics(:first).touch + assert_equal [transaction], transactions + end + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) + end + + def test_start_transaction_is_not_triggered_for_ordinary_nested_calls + transactions = [] + subscriber = ActiveSupport::Notifications.subscribe("start_transaction.active_record") do |event| + assert event.payload[:connection] + transactions << event.payload[:transaction] + end + + Topic.transaction do |t1| + topics(:first).touch + assert_equal [t1], transactions + + Topic.transaction do |_t2| + topics(:first).touch + assert_equal [t1], transactions + end + end + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) + end + + def test_start_transaction_is_triggered_for_requires_new + transactions = [] + subscriber = ActiveSupport::Notifications.subscribe("start_transaction.active_record") do |event| + assert event.payload[:connection] + transactions << event.payload[:transaction] + end + + Topic.transaction do |t1| + topics(:first).touch + assert_equal [t1], transactions + + Topic.transaction(requires_new: true) do |t2| + topics(:first).touch + assert_equal [t1, t2], transactions + end + end + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) + end + def test_transaction_instrumentation_on_commit topic = topics(:fifth) diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 78259b8815..507feaa339 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -410,9 +410,56 @@ This event is only emitted when [`config.active_record.action_on_strict_loading_ } ``` +#### `start_transaction.active_record` + +This event is emitted when a transaction has been started. + +| Key | Value | +| -------------------- | ---------------------------------------------------- | +| `:transaction` | Transaction object | +| `:connection` | Connection object | + +Please, note that Active Record does not create the actual database transaction +until needed: + +```ruby +ActiveRecord::Base.transaction do + # We are inside the block, but no event has been triggered yet. + + # The following line makes Active Record start the transaction. + User.count # Event fired here. +end +``` + +Remember that ordinary nested calls do not create new transactions: + +```ruby +ActiveRecord::Base.transaction do |t1| + User.count # Fires an event for t1. + ActiveRecord::Base.transaction do |t2| + # The next line fires no event for t2, because the only + # real database transaction in this example is t1. + User.first.touch + end +end +``` + +However, if `requires_new: true` is passed, you get an event for the nested +transaction too. This might be a savepoint under the hood: + +```ruby +ActiveRecord::Base.transaction do |t1| + User.count # Fires an event for t1. + ActiveRecord::Base.transaction(requires_new: true) do |t2| + User.first.touch # Fires an event for t2. + end +end +``` + #### `transaction.active_record` -This event is emmited for every transaction to the database. +This event is emitted when a database transaction finishes. The state of the +transaction can be found in the `:outcome` key. | Key | Value | | -------------------- | ---------------------------------------------------- | @@ -420,10 +467,9 @@ This event is emmited for every transaction to the database. | `:outcome` | `:commit`, `:rollback`, `:restart`, or `:incomplete` | | `:connection` | Connection object | -Please note that at this point the transaction has been finished, and its state -is in the `:outcome` key. In practice, you cannot do much with the transaction -object, but it may still be helpful for tracing database activity. For example, -by tracking `transaction.uuid`. +In practice, you cannot do much with the transaction object, but it may still be +helpful for tracing database activity. For example, by tracking +`transaction.uuid`. ### Action Mailer