As my understanding, if we have any code inside this transaction and when it happens any error with (save!, ...) in that block the entire code will revert, here the problem is if any timeout (rack timeout = 12) happens in this block.
def create
ActiveRecord::Base.transaction do
// timeout happens
end
end
How can we rollback a code with a transaction when a Rack::Timeout occurs?
When a Rack timeout happens, any transaction in process will be rolled back, but transactions that have already been committed will, of course remain committed. You should not have to worry about it.
Once you start a database transaction, it will eventually either be committed or rolled back. Those are the only two possibilities for ending a transaction. When you commit the transaction, you are saying that you want those changes to be saved regardless of what happens next. If you do not commit the transaction, the database will automatically rollback the transaction once it gets into any state where the transaction cannot move forward, such as a broken network connection.
ActiveRecord automatically commits the transaction when the ActiveRecord::Base.transaction do block exits normally. Abnormal exits may cause ActiveRecord to issue a ROLLBACK command to the database, which is efficient and good practice and returns the connection to a ready state, but it is not strictly necessary, because unless the transaction is explicitly committed, the database will eventually automatically roll it back.
If you look at ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction on line 270 and 283 Rails is rescuing Exception. Rescuing Exception will catch anything and everything including kill commands and should generally be avoided. It is used in this case to ensure that no matter what is raised (including Rack::Timeout) the transaction will rollback.
You need to explicitly specify an error class, then it will be rescued and ActiveRecord will rollback the transaction.
e.g. Timeout.timeout(1, Timeout::Error) do
https://ruby-doc.org/stdlib-2.4.0/libdoc/timeout/rdoc/Timeout.html
The exception thrown to terminate the given block cannot be rescued
inside the block unless klass is given explicitly.
Without it ActiveRecord thinks that there is no error and makes a COMMIT.
It seems it was a default behavior until ruby 2.1
Related
I've never done that before and I don't know if it is possible. I've got two process which I thing should most likely be wrapped in a database transaction to have the guarantee that either all changes in the database from the transaction will be made or no changes will be made.
process.campaign_code.update(state: 'used')
Campaign.find(process.campaign_code.campaign_id).increment!(:used_campaign_codes_amount, 1)
Try ActiveRecord::Base.transaction:
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
ActiveRecord::Base.transaction do
process.campaign_code.update!(state: 'used')
Campaign.find(process.campaign_code.campaign_id).increment!(:used_campaign_codes_amount, 1)
end
If any of the commands in the transaction throw an error, all SQL transactions in the block will be reverted.
EDIT: It's also worth changing update to update! to ensure an error is thrown if the update is unsuccessful, otherwise it can fail without throwing an error and the block will continue.
I am wondering what does the querying if the connection is in transaction or not actually do ?
Example :
....
try
if not DATA_MODULE.ACRDatabase1.InTransaction then
DATA_MODULE.ACRDatabase1.StartTransaction;
....
DATA_MODULE.ACRDatabase1.Commit();
except
DATA_MODULE.ACRDatabase1.Rollback;
Does it temporarily stop the current transaction if it detects that there is another transaction going on and waits for the other transaction to complete and only then executes or what? Or does it just misfire (rollback) if there's another transaction detected?
Attempting to start a transaction which has already been started will raise an exception. The call to InTransaction simply determines if the transaction has already started or not and returns a True/False response.
I prefer this...it protects you if you have any problems while editing...and only rollback if you have a problem with the Commit. If any exception is raised after the StartTransaction...you will never get to the Commit. You will always run the finally and will make sure you are not in a Tranaction, if so Rollback. I try not to use Try Except, don't have to worry about the Raise
try
DATA_MODULE.ACRDatabase1.StartTransaction;
....
DATA_MODULE.ACRDatabase1.Commit();
finally
if DATA_MODULE.ACRDatabase1.InTransaction then
DATA_MODULE.ACRDatabase1.Rollback;
at the very least the code should be reraising the exception or your user will never know why the data is not saving. ReRaising Exception
What could trigger a deadlock-message on Firebird when there is only a single transaction writing to the DB?
I am building a webapp with a backend written in Delphi2010 on top of a Firebird 2.1 database. I am getting an concurrent-update error that I cannot make sense of. Maybe someone can help me debug the issue or explain scenarios that may lead to the message.
I am trying an UPDATE to a single field on a single record.
UPDATE USERS SET passwdhash=? WHERE (RECID=?)
The message I am seeing is the standard:
deadlock
update conflicts with concurrent update
concurrent transaction number is 659718
deadlock
Error Code: 16
I understand what it tells me but I do not understand why I am seeing it here as there are no concurrent updates I know of.
Here is what I did to investigate.
I started my appplication server and checked the result of this query:
SELECT
A.MON$ATTACHMENT_ID,
A.MON$USER,
A.MON$REMOTE_ADDRESS,
A.MON$REMOTE_PROCESS,
T.MON$STATE,
T.MON$TIMESTAMP,
T.MON$TOP_TRANSACTION,
T.MON$OLDEST_TRANSACTION,
T.MON$OLDEST_ACTIVE,
T.MON$ISOLATION_MODE
FROM MON$ATTACHMENTS A
LEFT OUTER JOIN MON$TRANSACTIONS T
ON (T.MON$ATTACHMENT_ID = A.MON$ATTACHMENT_ID)
The result indicates a number of connections but only one of them has non-NULLs in the MON$TRANSACTION fields. This connection is the one I am using from IBExperts to query the monitor-tables.
Am I right to think that connection with no active transaction can be disregarded as not contributing to a deadlock-situation?
Next I put a breakpoint on the line submitting the UPDATE-Statement in my application server and executed the request that triggers it. When the breakpoint stopped the application I then reran the Monitor-query above.
This time I could see another transaction active just as I would expect:
Then I let my appserver execute the UPDATE and reap the error-message as shown above.
What can trigger the deadlock-message when there is only one writing transaction? Or are there more and I am misinterpreting the output? Any other suggestions on how to debug this?
Firebird uses MVCC (Multiversion Concurrency Control) for its transaction model. One of the features is that - depending on the transaction isolation - you will only see the last version committed when your transaction started (consistency and concurrency isolation levels), or that were committed when your statement started (read committed). A change to a record will create a new version of the record, which will only become visible to other active transactions when it has been committed (and then only for read committed transactions).
As a basic rule there can only be one uncommitted version of a record. So attempts by two transactions to update the same record will fail for one of those transaction. For historical reasons these type of errors are grouped under the deadlock error family, even though it is not actually a deadlock in the normal concurrency vernacular.
This rule is actually a bit more restrictive depending on your transaction isolation: for consistency and concurrency level there can also be no newer committed versions of a record that is not visible to your transaction.
My guess is that for you something like this happened:
Transaction 1 started
Transaction 2 started with concurrency or consistency isolation
Transaction 1 modifies record (new version created)
Transaction 1 commits
Transaction 2 attempts to modify same record
(Note, step 1+3 and 2 could be in a different order (eg 1,3,2, or 2,1,3))
Step 5 fails, because the new version created in step 3 is not visible to transaction 2. If instead read committed had been used then step 5 would succeed as the new version would be visible to the transaction at that point.
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
Is there any way for a before_save filter to halt the entire save without halting the transaction? What I'm trying to do is have a "sample" version of my model that the user can interact with and save but the changes themselves are never actually saved. The following will halt the transaction and (naturally) return false when I call #model.update_attributes:
before_filter :ignore_changes_if_sample
def ignore_changes_if_sample
if self.sample?
return false
end
end
Thanks!
That's precisely what's happening here. If you look at your SQL, you should be seeing BEGIN and then COMMIT, without anything between them. The before_save is not halting the transaction; it's simply preventing the record from being saved by returning false.
To more generally answer your question, records that fail to persist do not halt transactions unless they also raise an exception. Exceptions trigger the ROLLBACK that prevents any part of the transaction from being committed. So even if you return false here, a larger, overarching transaction should continue just fine.
You can read more about transactions and how Rails uses them in the ActiveRecord::Transactions documentation.