Ruby on Rails - rollback on transactions doesnt work for multiple saves - ruby-on-rails

I have this simple piece of code.
ActiveRecord::Base.transaction do
User.new(username: "michael").save!
User.new(username: "lebron").save!
User.new(username: "michael").save! # not unique error
end
Fun fact is that error in the 3rd line doesn't rollback all saves, but just the last one.
What am I not understanding about TX in Ruby so it behaves like that?
I know from documentation that
Both #save and #destroy come wrapped in a transaction
So I assume they are wrapped into a different one then the parent transaction.
Thuss making rollback from the 3rd statement not effective on the two prior ones.
How that should be addressed, so I have all statements rolled back during the course of the transaction?

Actually, ActiveRecord::Base.transaction will verify a single transaction so you can convert this into a single transaction so that it will work properly. So push user details into an array and create it.
Example:
user_details = [{username: "michael"}, {username: "lebron"}, {username: "michael"}] User.create(user_details)

Related

Ruby on Rails 7 ABORT/ROLLBACK when you have nested database transactions

I just updated to Rails 7 and am encountering this issue that used to work with Rails 6.1.
# new_object_controller.rb
ActiveRecord::Base.transaction do
new_object = NewObject.create!(params)
DoSomethingWithNewObjectService.new(new_object).do_something
end
# do_something_with_new_object_service.rb
def do_something
ActiveRecord::Base.transaction do
new_object.update!(blah_atributes)
end
end
When I step through the code with pry-debugger, I get these lines although they don't quite make sense to me:
From: .../vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3/lib/active_record/connection_adapters/abstract/transaction.rb:331 ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction:
it goes into lines 332, 336, 342 basically aborting and finally going into
TRANSACTION (5.1ms) ROLLBACK
From: .../vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:27 ActiveSupport::Concurrency::LoadInterlockAwareMonitor#synchronize:
The code actually goes through the end of the controller and tries to go to the next step but everything within the database transaction is rolled back.
Removing both the ActiveRecord::Base.transaction fixes the issue. Googling does not show much reasoning except this Rails 7 adds optional transaction arguments to with_lock.
This has many implications on design pattern as I use database transactions in small ruby classes that gets used in many places. If you can only have one transaction going at one time, that means that you need to be aware of any code executing within that transaction to ensure that there are no nested transactions.

Sum returns value when should return 0

I'm having a really confusing problem. Here it is:
[12] pry(EstimatedTime)> EstimatedTime.where(user_id: User.current.id, plan_on: date).pluck(:hours) => []
[13] pry(EstimatedTime)> EstimatedTime.where(user_id: User.current.id, plan_on: date).sum(:hours) => 3.0
What kind of magic is this?
This statement resides in model method, that is being called from view. Before that, in controller action, i'm calling another method of the same model, that is performing bulk create of records within transaction.
def self.save_all(records)
transaction do
records.each do |record|
record.save!
end
end
rescue ActiveRecord::RecordInvalid
return false
end
The exception is being thrown, method returns false, view is rendered and this happens.
UPD
I found a workaround, replacing .sum with .pluck(:hours).sum, but I still have no idea why my first way of doing this fails.
As David Aldrige pointed out in comments to the question, the problem was that .sum(:hours) was using cached data, while .pluck(:hours) was actually looking into database.
Why database and query cache contained different data? Well, seems like in Rails 3 (I should probably have specified my rails version when asking question) failed transaction would rollback the database, but leave query cache intact. This led to temporary data inconsistency between database and cache. This was corrected in Rails 4 as pointed out in this issue.
Solution 1 aka the one I chose for myself
Clear the query cache after transaction fails. So, the method performing the mass insert within transaction now looks like this:
def self.save_all(records)
transaction do
records.each do |record|
record.save!
end
end
rescue ActiveRecord::RecordInvalid
self.connection.clear_query_cache
return false
end
Solution 2
I personally find this much less elegant, though I cannot compare these solutions performance-wise, so I'll post them both. One can simply write the sum statement like this:
EstimatedTime.where(user_id: User.current.id, plan_on: date).pluck(:hours).sum
It will use the database data to calculate sum, bypassing the inconsistent cache problem.

ActiveRecord: update_all shows query in debug log, but doesn't execute in DB?

I've got the following update query running in a function called by a before_destroy callback in a Rails model:
Annotation.joins(:annotation_groups)
.where({'annotation_groups.group_id' => self.id})
.update_all({qc_approved: false}) if doc.in_qc?`
(I've also tried the following simpler version to see if another angle works: self.annotations.update_all({qc_approved: false}))
Both generate the below SQL query in "Server development log" (debugging in RubyMine):
UPDATE "annotations" SET "qc_approved" = 'f' WHERE "annotations"."id" IN (SELECT "annotations"."id" FROM "annotations" INNER JOIN "annotation_groups" ON "annotation_groups"."annotation_id" = "annotations"."id" WHERE "annotation_groups"."group_id" = 159)
However, as far as I can tell, that SQL never causes a DB update, even though the destroy process afterwards works fine. I can set a breakpoint directly after the statement and look at the database, and the qc_approved fields are still true. However, I can copy and paste the statement into a Postgres console and run it, and it updates the fields correctly.
Is anyone aware as to what would cause this behavior? Does before_destroy exist in its own strange alternate transactional universe that causes odd behavior like this? What scenario would cause the SQL to show up in the server log but not make it to the DB?
Thanks to the quick and helpful comments above confirming the nature of the callback inside the larger transaction, I figured it out.. despite the name, before_destroy was actually executing after dependent destroy calls, so that the joined annotation_group table row was destroyed before the UPDATE statement that relied on it was called in the transaction.
To be more specific, I added :prepend => true to the before_destroy definition so that it ran before the destroys as intended.

Do rails Transactions blocks exit after all actions have been committed?

Related to Run rails code after an update to the database has commited, without after_commit, but I think deserving its own question.
If I have code like this:
my_instance = MyModel.find(1)
MyModel.transaction do
my_instance.foo = "bar"
my_instance.save!
end
new_instance = MyModel.find(1)
puts new_instance.foo
Is this a guarantee that new_instance.foo will always output "bar" and not its previous value? I'm looking for a way to ensure that all the database actions that occur in a previous statement are committed BEFORE executing my next statements. Rails has an after_commit hook for this, but I don't want this code executed every time... only in this specific context.
I can't find anything in the documentation on Transactions that would indicate if Transaction blocks are "blocking". If they are blocking, that will satisfy my requirement. Unfortunately, I can't think of a practical way to test this behavior to confirm my suspicions one way or another.
Still researching this, but I think a transaction does block code execution until after the database confirms that it has written. Since "save!" is automatically wrapped in a transaction by Rails, the relevant code should run synchronously. The extra transaction block should be unnecessary.
I don't think Rails returns as soon as it hands off the call to the DB when the DB calls are within a transaction. The confusion I had was with after_save callbacks. After_save callbacks suffer from race conditions because they are in fact part of the transaction that saves are automatically wrapped in, so any code called by an after_save callback is not race condition safe, it is not protected by the transaction. Only after_commit calls are safe. Within the transaction Rails will hand off to the DB and then execute after_save callbacks before the DB has finished committing.
Studying this for more insights:
https://github.com/rails/rails/blob/bfdd3c2182156fa2cb81ed4f048b065a2d6f1341/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
UPDATE
Changing my answer to "no". It doesn't appear that save! or save blocks execution. From these two resources, looks like this is a common problem:
https://github.com/resque/resque/wiki/FAQ#how-do-you-make-a-resque-job-wait-for-an-activerecord-transaction-commit
https://blog.engineyard.com/2011/the-resque-way

Run rails code after an update to the database has commited, without after_commit

I'm trying to battle some race cases with my background task manager. Essentially, I have a Thing object (already exists) and assign it some properties, and then save it. After it is saved with the new properties, I queue it in Resque, passing in the ID.
thing = Thing.find(1)
puts thing.foo # outputs "old value"
thing.foo = "new value"
thing.save
ThingProcessor.queue_job(thing.id)
The background job will load the object from the database using Thing.find(thing_id).
The problem is that we've found Resque is so fast at picking up the job and loading the Thing object from the ID, that it loads a stale object. So within the job, calling thing.foo will still return "old value" like 1/100 times (not real data, but it does not happen often).
We know this is a race case, because rails will return from thing.save before the data has actually been commit to the database (postgresql in this case).
Is there a way in Rails to only execute code AFTER a database action has commit? Essentially I want to make sure that by the time Resque loads the object, it is getting the freshest object. I know this can be achieved using an after_commit hook on the Thing model, but I don't want it there. I only need this to happen in this one specific context, not every time the model has commit changed to the DB.
You can put in a transaction as well. Just like the example below:
transaction do
thing = Thing.find(1)
puts thing.foo # outputs "old value"
thing.foo = "new value"
thing.save
end
ThingProcessor.queue_job(thing.id)
Update: there is a gem which calls After Transaction, with this you may solve your problem. Here is the link:
http://xtargets.com/2012/03/08/understanding-and-solving-race-conditions-with-ruby-rails-and-background-workers/
What about wrapping a try around the transaction so that the job is queued up only upon success of the transaction?
I had a similar issue, where by I needed to ensure that a transaction had commited before running a series of action. I ended up using this Gem:
https://github.com/Envek/after_commit_everywhere
It meant that I could do the following:
def finalize!
Order.transaction do
payment.charge!
# ...
# Ensure that we only send out items and perform after actions when the order has defintely be completed
after_commit { OrderAfterFinalizerJob.perform_later(self) }
end
end
One gem to allow that is https://github.com/Ragnarson/after_commit_queue
It is a little different than the other answer's after_commit_everywhere gem. The after_commit_everywhere call seems decoupled from current model being saved or not.
So it might be what you expect or not expect, depending on your use case.

Resources