Skip to content

Healthy DB connections are thrown away after transaction serialization failure #43130

@eric-christian

Description

@eric-christian

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.

  1. 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
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions