Search code examples
ruby-on-railsactiverecord

What ActiveRecord Transaction doesn't rollback?


I want an ActiveRecord transaction to fail on purpose. The issue is that the rollback isn't happening when I raise an ActiveRecord::Rollback exception (or any other exception). In the code bellow, the perfom method imports a file. For the sake of simplicity, let's assume it just updates a record:

puts "+++ Before Transaction +++"

ActiveRecord::Base.transaction do
  puts "+++ In Transaction +++"
  perform

  puts "+++ Before Rollback +++"
  raise ActiveRecord::Rollback
end

puts "+++ After Transaction +++"

When the code runs here's the output of the server:

+++ Before Transaction +++
+++ In Transaction +++
TRANSACTION (0.1ms)  BEGIN
↳ app/models/import.rb:103:in `file_content'
Disk Storage (0.1ms) Downloaded file from key: abcdefg
Car Load (0.2ms)  SELECT "cars".* FROM "cars" WHERE "cars"."make" = '207' LIMIT 1
↳ app/models/application_record.rb:82:in `try_update_or_create_report'
Car Exists? (0.3ms)  SELECT 1 AS one FROM "rolling_shutters_automatism_prices" WHERE 
"cars"."make" = '207' AND "cars"."id" != 5 LIMIT 1
↳ app/models/application_record.rb:106:in `try_update_report'
Car Update (0.2ms)  UPDATE "cars" SET "price" = 20.0, "updated_at" = '2023-12-26 15:17:47.978023' WHERE "cars"."id" = 5
↳ app/models/application_record.rb:106:in `try_update_report'
+++ Before Rollback +++
+++ After Transaction +++

The behaviour I would expect is to have ROLLBACK logged in between the 2 last line, but nothing is logged. Why is that?

I use rails 7.0.4 with postgres 15 and ruby 3.2.2

EDIT: I left out an important piece of information. The transaction happens within an ActiveRecord callback.


Solution

  • The issue was that the transaction happened within an ActiveRecord callback. Since the callback is itself wrapped in a transaction, the transaction I was trying to perform was a nested transaction. Therefore, raising an ActiveRecord::Rollback exception caused the outer transaction to rollback.

    According to the documentation:

    In order to get a ROLLBACK for the nested transaction you may ask for a real sub-transaction by passing requires_new: true. If anything goes wrong, the database rolls back to the beginning of the sub-transaction without rolling back the parent transaction.

    So the fixed example would look like this:

    puts "+++ Before Transaction +++"
    
    ActiveRecord::Base.transaction(requires_new: true) do
      puts "+++ In Transaction +++"
      perform
    
      puts "+++ Before Rollback +++"
      raise ActiveRecord::Rollback
    end
    
    puts "+++ After Transaction +++"