How to know why a transaction was rolled back? - ruby-on-rails

I have an Account model. It has usual validations and after_save callbacks. There is a requirement, and account objects are to be created following some additional validation strategy. I have the following snippet, which works fine:
def special_account_creation
Account.transaction do
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
# rescue Rollback ??? <--- How to do this
# msg = <transaction_error_details>
# Rails.logger.info("Special Account not created due to: #{msg}")
end
end
a account is destroyed if anything goes wrong while calling a third party API or during validation.
I want to know how I can log the error to know why the transaction was rolled back.

ActiveRecord transaction catches any exception being raised inside and rolls back the transaction. After that, if the exception is ActiveRecord::Rollback, it is supressed otherwise any other exception is raised above the call stack.
Ref: https://api.rubyonrails.org/classes/ActiveRecord/Rollback.html
So you could do something like this:
Account.transaction do
begin
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue StandardError => e # <- assuming all errors need to be logged
Rails.logger.info("Special Account not created due to: #{msg}")
raise e # <-- Don't forget to raise the error!
end
end
I suggest raise the error from the rescue block - this will rollback the transaction and if the error is something other than ActiveRecord::Rollback, it will be raised outside the transaction block. These errors you can handle in rescue_from block in base controller.
Also prefer catching StandardError and not Exception: https://robots.thoughtbot.com/rescue-standarderror-not-exception
I am assuming we want to catch all possible errors in that block, hence the use of StandardError, otherwise use any specific errors that need to be intercepted.

ActiveRecord does not raise Rollback on failed update and/or failed validation.
You should rescue what was raised and then there are two opportunities: either raise Rollback to just rollback a transaction and continue normal execution flow, or re-raise what was raised to indeed break the normal flow.
def special_account_creation
Account.transaction do
a = Account.create(params)
begin
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue => e # might be more specific
Rails.logger.info("Transaction failed with a message #{e.message}")
raise ActiveRecord::Rollback, e.message # just to rollback
# raise e # for re-raising the exception
end
end
end

Related

Ruby on Rails Exception usage

Upon finding a failed validation, invalid? will return false and exit.
If all validations pass, invalid? will return true and the code will continue.
Does the rescue code only run if all validations pass?
If so, what raised errors will it catch?
Lastly why is there no Begin?
def save
return false if invalid? # invalid? triggers validations
true
rescue ActiveRecord::StatementInvalid => e
# Handle exception that caused the transaction to fail
# e.message and e.cause.message can be helpful
errors.add(:base, e.message)
false
end
Does the rescue code only run if all validations pass?
Blockquote
No, it will run if the call of invalid? throws an exception of type StatementInvalid
what raised errors will it catch?
Blockquote
the call of invalid? here is what raises the error
why is there no Begin?
in ruby, you can remove begin if you rescue from any exception that is raised from methods body so
def method
begin
#some code
rescue
#handle
end
end
equal to
def method
some code
rescue
# handle
end
but the second syntax shorter and cleaner
Note: it doesn't same right to me to rescue from ActiveRecord::StatementInvalid
inside an override to save

Execute method on exception raised without rescuing

I'm using Sidekiq, and I'd like to execute a method when an exception is raised in a background job, without rescuing the error.
Currently, I have:
begin
# job
rescue => SomeError
# run method
end
However, since it rescues the error, it does not go in the "failed" section of Sidekiq.
I'd like to execute code when an exception is raised, but without rescuing it. How can I do this ?
Just re-raise and you are all set.
begin
# job
rescue => SomeError
# run method
raise
end

ActiveRecord transactions not raising errors

I've set up an ActiveRecord transaction, however when the second statement fails, it's not causing the transaction to fail. Here's my code:
Contact.transaction do
contact = Contact.create(params)
channel = ContactChannel.create(contact: contact, phone: contact.phone)
# ContactChannel query raises a validation error
# puts "ERRORS: #{channel.errors.messages}" outputs the following:
# {:channel_key=>["has already been taken"]}
contact # Still returns the contact that was created
end
Any idea why this doesn't fail despite the validation error?
create! instead of create should raise an exception, which should cause the transaction to be rolled back. It is basically a more strict version, and if no exception is raised, the transaction does not fail.
To get the reason for the transaction rollback, you can wrap your Transaction-statement in an begin (...) rescue - block and catch the ActiveRecord::Rollback- error and use its message to return the reason for the transaction failure.

Non-fatal rescue in Rails

I'm trying to run a command that might fail sometimes. When it fails, it throws an exception.
What I'd like it to do is just log the error quietly and continue executing the next line below it, rather than aborting and going into the 'rescue' block. How should I approach this?
My current code is as follows:
rescue_from 'Gibbon::MailChimpError' do |exception|
logger.error("MAILCHIMP: #{exception}")
end
When I call the Mailchimp API, sometimes there is an error, and this disrupts the flow of my application. I just want it to carry on executing as if nothing has happened, and just note there was an error in the log.
How about something like this:
def rescuing(&block)
begin
yield
rescue NameError => e
puts "(Just rescued: #{e.inspect})"
end
end
rescuing do
puts "This is dangerous"
raise NameError
end
puts "... but I'm still alive"
Obviously, you'd have to replace NameError with the exception you want to be protected against.

Ruby on Rails: how do I catch ActiveRecord::Rollback?

In my controller, I have code that looks like the following:
#mymodel.transaction do
for a in arr
#mymodel.some_method(a)
end
end
in #mymodel#some_method I could throw an ActiveRecord::Rollback exception which in the db does what it needs to do, however I then simply get an HTTP 500 and no way to catch the exception to let the user know in an elegant way what went wrong.
I've tried wrapping #mymodel.transaction do in a begin/rescue block, but that won't do it either. What's the best way to catch the exception so I can present the proper view to the user?
From the ActiveRecord::Base documentation:
Normally, raising an exception will cause the transaction method to rollback the database transaction and pass on the exception. But if you raise an ActiveRecord::Rollback exception, then the database transaction will be rolled back, without passing on the exception.
A small example:
class ThrowController < ApplicationController
def index
status = ActiveRecord::Base.connection.transaction do
raise ActiveRecord::Rollback.new
end
Rails.logger.info "followed transaction"
end
end
then:
>> c = ThrowController.new.index
=> "followed transaction \n"
As you can see, the ActiveRecord:::Rollback exception is swallowed by the transaction block.
It seems to me that something else is going on with your code that we're not aware of.

Resources