Skip to content

Commit 57bc28f

Browse files
committed
Add new tests for deferred connection verification and auto-reconnect
1 parent 6693e5f commit 57bc28f

File tree

14 files changed

+304
-165
lines changed

14 files changed

+304
-165
lines changed

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,8 @@ def commit_db_transaction() end
389389
# done if the transaction block raises an exception or returns false.
390390
def rollback_db_transaction
391391
exec_rollback_db_transaction
392+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::ConnectionFailed
393+
reconnect!
392394
end
393395

394396
def exec_rollback_db_transaction() end # :nodoc:
@@ -478,6 +480,10 @@ def high_precision_current_timestamp
478480
end
479481

480482
private
483+
def internal_execute(sql, name = "SCHEMA")
484+
execute(sql, name)
485+
end
486+
481487
def execute_batch(statements, name = nil)
482488
statements.each do |statement|
483489
execute(statement, name)

activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ def current_savepoint_name
88
end
99

1010
def create_savepoint(name = current_savepoint_name)
11-
execute("SAVEPOINT #{name}", "TRANSACTION")
11+
internal_execute("SAVEPOINT #{name}", "TRANSACTION")
1212
end
1313

1414
def exec_rollback_to_savepoint(name = current_savepoint_name)
15-
execute("ROLLBACK TO SAVEPOINT #{name}", "TRANSACTION")
15+
internal_execute("ROLLBACK TO SAVEPOINT #{name}", "TRANSACTION")
1616
end
1717

1818
def release_savepoint(name = current_savepoint_name)
19-
execute("RELEASE SAVEPOINT #{name}", "TRANSACTION")
19+
internal_execute("RELEASE SAVEPOINT #{name}", "TRANSACTION")
2020
end
2121
end
2222
end

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -403,24 +403,24 @@ def restorable?
403403
def materialize_transactions
404404
return if @materializing_transactions
405405

406+
if @has_unmaterialized_transactions
407+
@connection.lock.synchronize do
408+
begin
409+
@materializing_transactions = true
410+
@stack.each { |t| t.materialize! unless t.materialized? }
411+
ensure
412+
@materializing_transactions = false
413+
end
414+
@has_unmaterialized_transactions = false
415+
end
416+
end
417+
406418
# As a logical simplification for now, we assume anything that requests
407419
# materialization is about to dirty the transaction. Note this is just
408420
# an assumption about the caller, not a direct property of this method.
409421
# It can go away later when callers are able to handle dirtiness for
410422
# themselves.
411423
dirty_current_transaction
412-
413-
return unless @has_unmaterialized_transactions
414-
415-
@connection.lock.synchronize do
416-
begin
417-
@materializing_transactions = true
418-
@stack.each { |t| t.materialize! unless t.materialized? }
419-
ensure
420-
@materializing_transactions = false
421-
end
422-
@has_unmaterialized_transactions = false
423-
end
424424
end
425425

426426
def commit_transaction

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ def with_raw_connection(allow_retry: false, uses_transaction: true)
917917
end
918918

919919
def retryable_connection_error?(exception)
920-
exception.is_a?(ConnectionNotEstablished)
920+
exception.is_a?(ConnectionNotEstablished) || exception.is_a?(ConnectionFailed)
921921
end
922922

923923
def retryable_query_error?(exception)

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,15 @@ def begin_isolated_db_transaction(isolation) # :nodoc:
224224
end
225225

226226
def commit_db_transaction # :nodoc:
227-
internal_execute("COMMIT", "TRANSACTION")
227+
internal_execute("COMMIT", "TRANSACTION", allow_retry: false, uses_transaction: true)
228228
end
229229

230230
def exec_rollback_db_transaction # :nodoc:
231-
internal_execute("ROLLBACK", "TRANSACTION")
231+
internal_execute("ROLLBACK", "TRANSACTION", allow_retry: false, uses_transaction: true)
232232
end
233233

234234
def exec_restart_db_transaction # :nodoc:
235-
internal_execute("ROLLBACK AND CHAIN", "TRANSACTION")
235+
internal_execute("ROLLBACK AND CHAIN", "TRANSACTION", allow_retry: false, uses_transaction: true)
236236
end
237237

238238
def empty_insert_statement_value(primary_key = nil) # :nodoc:
@@ -679,8 +679,12 @@ def internal_execute(sql, name = "SCHEMA", allow_retry: true, uses_transaction:
679679
ER_CANNOT_CREATE_TABLE = 1005
680680
ER_LOCK_WAIT_TIMEOUT = 1205
681681
ER_QUERY_INTERRUPTED = 1317
682+
ER_CONNECTION_KILLED = 1927
683+
CR_SERVER_GONE_ERROR = 2006
684+
CR_SERVER_LOST = 2013
682685
ER_QUERY_TIMEOUT = 3024
683686
ER_FK_INCOMPATIBLE_COLUMNS = 3780
687+
ER_CLIENT_INTERACTION_TIMEOUT = 4031
684688

685689
def translate_exception(exception, message:, sql:, binds:)
686690
case error_number(exception)
@@ -690,6 +694,8 @@ def translate_exception(exception, message:, sql:, binds:)
690694
else
691695
super
692696
end
697+
when ER_CONNECTION_KILLED, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT
698+
ConnectionFailed.new(message, sql: sql, binds: binds)
693699
when ER_DB_CREATE_EXISTS
694700
DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
695701
when ER_DUP_ENTRY

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def get_full_version
173173
def translate_exception(exception, message:, sql:, binds:)
174174
if exception.is_a?(Mysql2::Error::TimeoutError) && !exception.error_number
175175
ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds)
176+
elsif exception.is_a?(Mysql2::Error::ConnectionError)
177+
if exception.message.match?(/MySQL client is not connected/i)
178+
ActiveRecord::ConnectionNotEstablished.new(exception)
179+
else
180+
ActiveRecord::ConnectionFailed.new(message, sql: sql, binds: binds)
181+
end
176182
else
177183
super
178184
end

activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,18 @@ def begin_isolated_db_transaction(isolation) # :nodoc:
125125

126126
# Commits a transaction.
127127
def commit_db_transaction # :nodoc:
128-
internal_execute("COMMIT", "TRANSACTION")
128+
internal_execute("COMMIT", "TRANSACTION", allow_retry: false, uses_transaction: true)
129129
end
130130

131131
# Aborts a transaction.
132132
def exec_rollback_db_transaction # :nodoc:
133-
if @raw_connection
134-
@raw_connection.cancel unless @raw_connection.transaction_status == PG::PQTRANS_IDLE
135-
@raw_connection.block
136-
end
137-
internal_execute("ROLLBACK", "TRANSACTION")
133+
cancel_any_running_query
134+
internal_execute("ROLLBACK", "TRANSACTION", allow_retry: false, uses_transaction: true)
138135
end
139136

140137
def exec_restart_db_transaction # :nodoc:
141-
if @raw_connection
142-
@raw_connection.cancel unless @raw_connection.transaction_status == PG::PQTRANS_IDLE
143-
@raw_connection.block
144-
end
145-
internal_execute("ROLLBACK AND CHAIN", "TRANSACTION")
138+
cancel_any_running_query
139+
internal_execute("ROLLBACK AND CHAIN", "TRANSACTION", allow_retry: false, uses_transaction: true)
146140
end
147141

148142
# From https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
@@ -154,6 +148,13 @@ def high_precision_current_timestamp
154148
end
155149

156150
private
151+
def cancel_any_running_query
152+
return unless @raw_connection && @raw_connection.transaction_status != PG::PQTRANS_IDLE
153+
@raw_connection.cancel
154+
@raw_connection.block
155+
rescue PG::Error
156+
end
157+
157158
def execute_batch(statements, name = nil)
158159
execute(combine_multi_statements(statements))
159160
end

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,17 @@ def translate_exception(exception, message:, sql:, binds:)
680680
when nil
681681
if exception.message.match?(/connection is closed/i)
682682
ConnectionNotEstablished.new(exception)
683-
elsif exception.is_a?(PG::ConnectionBad) && !exception.message.end_with?("\n")
684-
ConnectionNotEstablished.new(exception)
683+
elsif exception.is_a?(PG::ConnectionBad)
684+
# libpq message style always ends with a newline; the pg gem's internal
685+
# errors do not. We separate these cases because a pg-internal
686+
# ConnectionBad means it failed before it managed to send the query,
687+
# whereas a libpq failure could have occurred at any time (meaning the
688+
# server may have already executed part or all of the query).
689+
if exception.message.end_with?("\n")
690+
ConnectionFailed.new(exception)
691+
else
692+
ConnectionNotEstablished.new(exception)
693+
end
685694
else
686695
super
687696
end

activerecord/lib/active_record/errors.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,11 @@ class QueryCanceled < QueryAborted
493493
class AdapterTimeout < QueryAborted
494494
end
495495

496+
# ConnectionFailed will be raised when the network connection to the
497+
# database fails while sending a query or waiting for its result.
498+
class ConnectionFailed < QueryAborted
499+
end
500+
496501
# UnknownAttributeReference is raised when an unknown and potentially unsafe
497502
# value is passed to a query method. For example, passing a non column name
498503
# value to a relation's #order method might cause this exception.

0 commit comments

Comments
 (0)