Define the new start_transaction.active_record event

This commit is contained in:
Xavier Noria 2024-06-10 17:30:37 +02:00
parent 0733ab5118
commit f64a4134df
3 changed files with 110 additions and 6 deletions

@ -91,7 +91,9 @@ def start
raise InstrumentationAlreadyStartedError.new("Called start on an already started transaction") if @started raise InstrumentationAlreadyStartedError.new("Called start on an already started transaction") if @started
@started = true @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 = ActiveSupport::Notifications.instrumenter.build_handle("transaction.active_record", @payload)
@handle.start @handle.start
end end

@ -7,6 +7,62 @@ class TransactionInstrumentationTest < ActiveRecord::TestCase
self.use_transactional_tests = false self.use_transactional_tests = false
fixtures :topics 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 def test_transaction_instrumentation_on_commit
topic = topics(:fifth) topic = topics(:fifth)

@ -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` #### `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 | | Key | Value |
| -------------------- | ---------------------------------------------------- | | -------------------- | ---------------------------------------------------- |
@ -420,10 +467,9 @@ This event is emmited for every transaction to the database.
| `:outcome` | `:commit`, `:rollback`, `:restart`, or `:incomplete` | | `:outcome` | `:commit`, `:rollback`, `:restart`, or `:incomplete` |
| `:connection` | Connection object | | `:connection` | Connection object |
Please note that at this point the transaction has been finished, and its state In practice, you cannot do much with the transaction object, but it may still be
is in the `:outcome` key. In practice, you cannot do much with the transaction helpful for tracing database activity. For example, by tracking
object, but it may still be helpful for tracing database activity. For example, `transaction.uuid`.
by tracking `transaction.uuid`.
### Action Mailer ### Action Mailer