Failsafe on ActiveRecord::ConnectionFailed - #317
Conversation
When the database server terminates a connection mid-query (e.g. Postgres idle_in_transaction_session_timeout, MySQL server gone away) the adapter raises ActiveRecord::ConnectionFailed. It is a StatementInvalid subclass (via QueryAborted), not a ConnectionNotEstablished, so the failsafe did not rescue it and the error propagated to the caller instead of degrading to a cache miss. Active Record itself treats ConnectionFailed as a retryable connection error alongside ConnectionNotEstablished, which is already in the list. Add it explicitly, keeping the list auditable. The emulating_timeouts test helper now accepts the error class to raise, so the new tests can cover ConnectionFailed in the established shape. Fixes rails#307
Cover every cache operation, not only read/write/delete/fetch. Same approach as rails#308.
|
Reviewed both this and #308. Ran each locally on Ruby 3.4.8 against sqlite: #308 is 400 runs / 0 failures, this one 405 runs / 0 failures. The production diff is byte identical in both, so the only thing to decide is the tests. I think the best outcome is a merge of the two, and I say that as the person who suggested the signature this PR uses. #308's helper naming is better than mine. I proposed The most valuable test here is not the ConnectionFailed one. It is So concretely: take #308 as the base, then rebase this one down to Two small things if this one is what gets merged instead:
Nothing blocking in either. On the timeline correction, thanks, you are right, and |
Fixes #307
When the database server terminates a connection mid-query, the adapter raises
ActiveRecord::ConnectionFailed. Postgres does this onidle_in_transaction_session_timeout(which is how we hit it:Entry.lock_and_writeholds a row lock while the block runs, and under load the session idles past the timeout), MySQL onER_CONNECTION_KILLED/CR_SERVER_GONE_ERROR/CR_SERVER_LOST, Trilogy on the equivalent.ConnectionFailedis a subclass ofQueryAborted/StatementInvalid, not ofConnectionNotEstablished, soTRANSIENT_ACTIVE_RECORD_ERRORSdid not match it and the error propagated to the caller instead of degrading to a cache miss.This adds it to the list. Active Record already treats it as a retryable connection error next to
ConnectionNotEstablished(AbstractAdapter#retryable_connection_error?), which is in the list. It is also the onlyQueryAbortedsubclass that was missing. I kept the list explicit rather than collapsing toQueryAborted, so future Active Record error classes have to be opted in on purpose.Tests:
emulating_timeoutsintest/test_helper.rbnow takes the error class to raise (default unchanged). A newSolidCacheConnectionFailedFailsafeTestintest/unit/solid_cache_test.rbruns the wholeFailureSafetyBehavioragainstConnectionFailed, so every cache operation is checked to degrade to a miss. Newtest/unit/failsafe_test.rbcovers the error handler contract:ConnectionFailedreaches theerror_handler, and is re-raised when the handler raises. It also checks thatStatementInvalidandNoDatabaseErrorstill propagate, since the point of the list (#182) is not to hide configuration errors.Overlaps with #308 by @ajaynomics, which has the same one-line change and the
FailureSafetyBehaviorapproach (taken from there). This one adds the error handler and non-transient error tests. Happy for either to land.Verified locally on SQLite with Rails 8.1.2.1: full suite green, and the two
ConnectionFailedtests fail without the one-line change tofailsafe.rb.