Skip to content

Commit 7fe221d

Browse files
authored
Merge pull request rails#44576 from rails/defer-db-verify
Defer verification of database connections
2 parents b623494 + 57bc28f commit 7fe221d

23 files changed

+732
-354
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ def checkout_new_connection
705705

706706
def checkout_and_verify(c)
707707
c._run_checkout_callbacks do
708-
c.verify!
708+
c.clean!
709709
end
710710
c
711711
rescue

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: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ def materialized?
142142
end
143143

144144
def restore!
145-
@materialized = false
145+
if materialized?
146+
@materialized = false
147+
materialize!
148+
end
146149
end
147150

148151
def rollback_records
@@ -390,8 +393,6 @@ def restore_transactions
390393

391394
@stack.each(&:restore!)
392395

393-
materialize_transactions unless @lazy_transactions_enabled
394-
395396
true
396397
end
397398

@@ -402,24 +403,24 @@ def restorable?
402403
def materialize_transactions
403404
return if @materializing_transactions
404405

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+
405418
# As a logical simplification for now, we assume anything that requests
406419
# materialization is about to dirty the transaction. Note this is just
407420
# an assumption about the caller, not a direct property of this method.
408421
# It can go away later when callers are able to handle dirtiness for
409422
# themselves.
410423
dirty_current_transaction
411-
412-
return unless @has_unmaterialized_transactions
413-
414-
@connection.lock.synchronize do
415-
begin
416-
@materializing_transactions = true
417-
@stack.each { |t| t.materialize! unless t.materialized? }
418-
ensure
419-
@materializing_transactions = false
420-
end
421-
@has_unmaterialized_transactions = false
422-
end
423424
end
424425

425426
def commit_transaction

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 179 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def initialize(connection, logger = nil, config = {}) # :nodoc:
114114
@default_timezone = self.class.validate_default_timezone(config[:default_timezone])
115115

116116
@raw_connection_dirty = false
117+
@verified = false
117118

118119
configure_connection
119120
end
@@ -145,6 +146,10 @@ def use_metadata_table?
145146
@config.fetch(:use_metadata_table, true)
146147
end
147148

149+
def connection_retries
150+
(@config[:connection_retries] || 3).to_i
151+
end
152+
148153
def default_timezone
149154
@default_timezone || ActiveRecord.default_timezone
150155
end
@@ -552,17 +557,42 @@ def all_foreign_keys_valid?
552557
def active?
553558
end
554559

555-
# Disconnects from the database if already connected, and establishes a
556-
# new connection with the database. Implementors should call super
557-
# immediately after establishing the new connection (and while still
558-
# holding @lock).
560+
# Disconnects from the database if already connected, and establishes a new
561+
# connection with the database. Implementors should define private #reconnect
562+
# instead.
559563
def reconnect!(restore_transactions: false)
560-
reset_transaction(restore: restore_transactions) do
561-
clear_cache!(new_connection: true)
562-
configure_connection
564+
retries_available = connection_retries
565+
566+
@lock.synchronize do
567+
reconnect
568+
569+
enable_lazy_transactions!
570+
@raw_connection_dirty = false
571+
@verified = true
572+
573+
reset_transaction(restore: restore_transactions) do
574+
clear_cache!(new_connection: true)
575+
configure_connection
576+
end
577+
rescue => original_exception
578+
translated_exception = translate_exception_class(original_exception, nil, nil)
579+
580+
if retries_available > 0
581+
retries_available -= 1
582+
583+
if retryable_connection_error?(translated_exception)
584+
backoff(connection_retries - retries_available)
585+
retry
586+
end
587+
end
588+
589+
@verified = false
590+
591+
raise translated_exception
563592
end
564593
end
565594

595+
566596
# Disconnects from the database if already connected. Otherwise, this
567597
# method does nothing.
568598
def disconnect!
@@ -628,7 +658,13 @@ def requires_reloading?
628658
# This is done under the hood by calling #active?. If the connection
629659
# is no longer active, then this method will reconnect to the database.
630660
def verify!
631-
reconnect! unless active?
661+
reconnect!(restore_transactions: true) unless active?
662+
@verified = true
663+
end
664+
665+
def clean! # :nodoc:
666+
@raw_connection_dirty = false
667+
@verified = nil
632668
end
633669

634670
# Provides access to the underlying database driver for this adapter. For
@@ -638,9 +674,11 @@ def verify!
638674
# This is useful for when you need to call a proprietary method such as
639675
# PostgreSQL's lo_* methods.
640676
def raw_connection
641-
disable_lazy_transactions!
642-
@raw_connection_dirty = true
643-
@raw_connection
677+
with_raw_connection do |conn|
678+
disable_lazy_transactions!
679+
@raw_connection_dirty = true
680+
conn
681+
end
644682
end
645683

646684
def default_uniqueness_comparison(attribute, value) # :nodoc:
@@ -796,6 +834,130 @@ def reconnect_can_restore_state?
796834
transaction_manager.restorable? && !@raw_connection_dirty
797835
end
798836

837+
# Lock the monitor, ensure we're properly connected and
838+
# transactions are materialized, and then yield the underlying
839+
# raw connection object.
840+
#
841+
# If +allow_retry+ is true, a connection-related exception will
842+
# cause an automatic reconnect and re-run of the block, up to
843+
# the connection's configured +connection_retries+ setting.
844+
#
845+
# If +uses_transaction+ is false, the block will be run without
846+
# ensuring virtual transactions have been materialized in the DB
847+
# server's state. The active transaction will also remain clean
848+
# (if it is not already dirty), meaning it's able to be restored
849+
# by reconnecting and opening an equivalent-depth set of new
850+
# transactions. This should only be used by transaction control
851+
# methods, and internal transaction-agnostic queries.
852+
#
853+
###
854+
#
855+
# It's not the primary use case, so not something to optimize
856+
# for, but note that this method does need to be re-entrant:
857+
# +materialize_transactions+ will re-enter if it has work to do,
858+
# and the yield block can also do so under some circumstances.
859+
#
860+
# In the latter case, we really ought to guarantee the inner
861+
# call will not reconnect (which would interfere with the
862+
# still-yielded connection in the outer block), but we currently
863+
# provide no special enforcement there.
864+
#
865+
def with_raw_connection(allow_retry: false, uses_transaction: true)
866+
@lock.synchronize do
867+
materialize_transactions if uses_transaction
868+
869+
retries_available = allow_retry ? connection_retries : 0
870+
reconnectable = reconnect_can_restore_state?
871+
872+
if @verified
873+
# Cool, we're confident the connection's ready to use. (Note this might have
874+
# become true during the above #materialize_transactions.)
875+
elsif reconnectable
876+
if allow_retry
877+
# Not sure about the connection yet, but if anything goes wrong we can
878+
# just reconnect and re-run our query
879+
else
880+
# We can reconnect if needed, but we don't trust the upcoming query to be
881+
# safely re-runnable: let's verify the connection to be sure
882+
verify!
883+
end
884+
else
885+
# We don't know whether the connection is okay, but it also doesn't matter:
886+
# we wouldn't be able to reconnect anyway. We're just going to run our query
887+
# and hope for the best.
888+
end
889+
890+
begin
891+
result = yield @raw_connection
892+
@verified = true
893+
result
894+
rescue => original_exception
895+
translated_exception = translate_exception_class(original_exception, nil, nil)
896+
897+
if retries_available > 0
898+
retries_available -= 1
899+
900+
if retryable_query_error?(translated_exception)
901+
backoff(connection_retries - retries_available)
902+
retry
903+
elsif reconnectable && retryable_connection_error?(translated_exception)
904+
reconnect!(restore_transactions: true)
905+
# Only allowed to reconnect once, because reconnect! has its own retry
906+
# loop
907+
reconnectable = false
908+
retry
909+
end
910+
end
911+
912+
raise translated_exception
913+
ensure
914+
dirty_current_transaction if uses_transaction
915+
end
916+
end
917+
end
918+
919+
def retryable_connection_error?(exception)
920+
exception.is_a?(ConnectionNotEstablished) || exception.is_a?(ConnectionFailed)
921+
end
922+
923+
def retryable_query_error?(exception)
924+
# We definitely can't retry if we were inside a transaction that was instantly
925+
# rolled back by this error
926+
if exception.is_a?(TransactionRollbackError) && savepoint_errors_invalidate_transactions? && open_transactions > 0
927+
false
928+
else
929+
exception.is_a?(Deadlocked) || exception.is_a?(LockWaitTimeout)
930+
end
931+
end
932+
933+
def backoff(counter)
934+
sleep 0.1 * counter
935+
end
936+
937+
def reconnect
938+
raise NotImplementedError
939+
end
940+
941+
# Returns a raw connection for internal use with methods that are known
942+
# to both be thread-safe and not rely upon actual server communication.
943+
# This is useful for e.g. string escaping methods.
944+
def any_raw_connection
945+
@raw_connection
946+
end
947+
948+
# Similar to any_raw_connection, but ensures it is validated and
949+
# connected. Any method called on this result still needs to be
950+
# independently thread-safe, so it probably shouldn't talk to the
951+
# server... but some drivers fail if they know the connection has gone
952+
# away.
953+
def valid_raw_connection
954+
(@verified && @raw_connection) ||
955+
# `allow_retry: false`, to force verification: the block won't
956+
# raise, so a retry wouldn't help us get the valid connection we
957+
# need.
958+
with_raw_connection(allow_retry: false, uses_transaction: false) { |conn| conn }
959+
end
960+
799961
def extended_type_map_key
800962
if @default_timezone
801963
{ default_timezone: @default_timezone }
@@ -831,11 +993,11 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
831993
type_casted_binds: type_casted_binds,
832994
statement_name: statement_name,
833995
async: async,
834-
connection: self) do
835-
@lock.synchronize(&block)
836-
rescue => e
837-
raise translate_exception_class(e, sql, binds)
838-
end
996+
connection: self,
997+
&block
998+
)
999+
rescue ActiveRecord::StatementInvalid => ex
1000+
raise ex.set_query(sql, binds)
8391001
end
8401002

8411003
def transform_query(sql)
@@ -848,7 +1010,7 @@ def transform_query(sql)
8481010
def translate_exception(exception, message:, sql:, binds:)
8491011
# override in derived class
8501012
case exception
851-
when RuntimeError
1013+
when RuntimeError, ActiveRecord::ActiveRecordError
8521014
exception
8531015
else
8541016
ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)

0 commit comments

Comments
 (0)