@@ -114,6 +114,7 @@ def initialize(connection, logger = nil, config = {}) # :nodoc:
114
114
@default_timezone = self . class . validate_default_timezone ( config [ :default_timezone ] )
115
115
116
116
@raw_connection_dirty = false
117
+ @verified = false
117
118
118
119
configure_connection
119
120
end
@@ -145,6 +146,10 @@ def use_metadata_table?
145
146
@config . fetch ( :use_metadata_table , true )
146
147
end
147
148
149
+ def connection_retries
150
+ ( @config [ :connection_retries ] || 3 ) . to_i
151
+ end
152
+
148
153
def default_timezone
149
154
@default_timezone || ActiveRecord . default_timezone
150
155
end
@@ -552,17 +557,42 @@ def all_foreign_keys_valid?
552
557
def active?
553
558
end
554
559
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.
559
563
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
563
592
end
564
593
end
565
594
595
+
566
596
# Disconnects from the database if already connected. Otherwise, this
567
597
# method does nothing.
568
598
def disconnect!
@@ -628,7 +658,13 @@ def requires_reloading?
628
658
# This is done under the hood by calling #active?. If the connection
629
659
# is no longer active, then this method will reconnect to the database.
630
660
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
632
668
end
633
669
634
670
# Provides access to the underlying database driver for this adapter. For
@@ -638,9 +674,11 @@ def verify!
638
674
# This is useful for when you need to call a proprietary method such as
639
675
# PostgreSQL's lo_* methods.
640
676
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
644
682
end
645
683
646
684
def default_uniqueness_comparison ( attribute , value ) # :nodoc:
@@ -796,6 +834,130 @@ def reconnect_can_restore_state?
796
834
transaction_manager . restorable? && !@raw_connection_dirty
797
835
end
798
836
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
+
799
961
def extended_type_map_key
800
962
if @default_timezone
801
963
{ default_timezone : @default_timezone }
@@ -831,11 +993,11 @@ def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name =
831
993
type_casted_binds : type_casted_binds ,
832
994
statement_name : statement_name ,
833
995
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 )
839
1001
end
840
1002
841
1003
def transform_query ( sql )
@@ -848,7 +1010,7 @@ def transform_query(sql)
848
1010
def translate_exception ( exception , message :, sql :, binds :)
849
1011
# override in derived class
850
1012
case exception
851
- when RuntimeError
1013
+ when RuntimeError , ActiveRecord :: ActiveRecordError
852
1014
exception
853
1015
else
854
1016
ActiveRecord ::StatementInvalid . new ( message , sql : sql , binds : binds )
0 commit comments