-
Notifications
You must be signed in to change notification settings - Fork 21.9k
Description
Hello :)
Last week we updated from 6.1.3.2 to 6.1.4.1 and noticed that there are many connections dropped and reestablished now.
We also got ActiveRecord::ConnectionNotEstablished within nested transactions in some special cases. (Explained in more detail at the bottom)
After some digging, i noticed that connections are thrown away after an ActiveRecord::TransactionRollbackError is raised. Which is the parent class for ActiveRecord::SerializationFailure and ActiveRecord::Deadlocked.
At least for the ActiveRecord::SerializationFailure exception, this behavior is giving us some trouble.
We are using the transaction isolation level "serializable", where SerializationFailure's are a common thing.
We are currently using CockroachDB but as far as i know neither mysql nor postgresql leaves the connection in an undefined state after a SerializationFailure. Simply rollback and retry the transaction on the same connection is a valid way to go. So dropping the connection is not necessary. If there is a case with a DB where dropping the connection is actually required, can we maybe only drop the connection in that case?
v6.1.3.2...v6.1.4#diff-353f4f101b1bb04938d761b6e29c88e0f147084e9e276854a77ae5d9a9a3b2a8R3
v6.1.3.2...v6.1.4#diff-dd0ea9dfea1fd486f2b1b5eac8c798f839467173a1c2315d3ad04f4098f7dda3R333
I don't know if this was meant to be a performance improvement or actually a real issue with some databases that i don't know of. But skipping a ROLLBACK and dropping a connection are two different things. I think, but i'm not 100% sure about it right now, that the ROLLBACK [SAVEPOINT ...] is still required after a SerializationFailure.
I think you have to signal the DB that you want to ROLLBACK the failed transaction if you attempt to retry it on the same connection. This probably depends on the DB? If there is different behavior, maybe leave it to the adapter to decide what way to go? So far i don't understand why this changes were made.
Example time! :)
We are currently running two different implementations of retryable transactions.
- A basic one with simple exponential backoff, which still works but issues unnecessary reconnects.
# simplified code
simple_retryable_transaction do
return yield if within_transaction? # no retries for nested transactions
loop do
break ActiveRecord::Base.connection.transaction { yield } # RealTransaction
rescue ActiveRecord::SerializationFailure => error
# update metrics / APM to see contention
raise unless more_retries?
sleep exponential_backoff_seconds
end
end
- A more specialised and faster one, where a savepoint is rolled back. Probably only featured by CockroachDB? This one is now broken. After the first SerializationFailure the Inner- and Outer-Transaction are not on the same connection anymore.
# simplified code
advanced_retryable_transaction do
return yield if within_transaction?
ActiveRecord::Base.connection.transaction(joinable: false) do
loop do
# on the second iteration, this will create a RealTransaction on a new connection.
break ActiveRecord::Base.connection.transaction(requires_new: true) { yield }
rescue ActiveRecord::SerializationFailure => error
# update metrics / APM to see contention
# since the DB is prioritising the session with the most retries during contention evaluation:
# no sleep and no limited amount of retries
end
# If we had a SerializationFailure during the loop which we solved by retrying,
# this transaction is now attempting to issue a COMMIT on a closed connection \o/
end
end
If required i'll try to put together some examples with https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#create-an-executable-test-case
Best regards,
Eric
Expected behavior
Keep the connection after ActiveRecord::SerializationFailure and probably even after ActiveRecord::Deadlocked.
Actual behavior
Connections are thrown away after ActiveRecord::SerializationFailure and ActiveRecord::Deadlocked.
System configuration
Rails version: 6.1.4.1
Ruby version: 2.7.3