Skip to content

Commit 8b544b2

Browse files
author
Matthew Draper
authored
Merge pull request rails#53672 from matthewd/last-activity
2 parents 2ab0094 + a4cefc1 commit 8b544b2

File tree

7 files changed

+69
-4
lines changed

7 files changed

+69
-4
lines changed

activerecord/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Remember when a database connection has recently been verified (for
2+
two seconds, by default), to avoid repeated reverifications during a
3+
single request.
4+
5+
This should recreate a similar rate of verification as in Rails 7.1,
6+
where connections are leased for the duration of a request, and thus
7+
only verified once.
8+
9+
*Matthew Draper*
10+
111
* Allow to reset cache counters for multiple records.
212

313
```

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def initialize(config_or_deprecated_connection, deprecated_logger = nil, depreca
168168
@default_timezone = self.class.validate_default_timezone(@config[:default_timezone])
169169

170170
@raw_connection_dirty = false
171+
@last_activity = nil
171172
@verified = false
172173
end
173174

@@ -217,6 +218,10 @@ def connection_retries
217218
(@config[:connection_retries] || 1).to_i
218219
end
219220

221+
def verify_timeout
222+
(@config[:verify_timeout] || 2).to_i
223+
end
224+
220225
def retry_deadline
221226
if @config[:retry_deadline]
222227
@config[:retry_deadline].to_f
@@ -343,6 +348,13 @@ def seconds_idle # :nodoc:
343348
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @idle_since
344349
end
345350

351+
# Seconds since this connection last communicated with the server
352+
def seconds_since_last_activity # :nodoc:
353+
if @raw_connection && @last_activity
354+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_activity
355+
end
356+
end
357+
346358
def unprepared_statement
347359
cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements
348360
yield
@@ -670,6 +682,7 @@ def reconnect!(restore_transactions: false)
670682

671683
enable_lazy_transactions!
672684
@raw_connection_dirty = false
685+
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
673686
@verified = true
674687

675688
reset_transaction(restore: restore_transactions) do
@@ -689,6 +702,7 @@ def reconnect!(restore_transactions: false)
689702
end
690703
end
691704

705+
@last_activity = nil
692706
@verified = false
693707

694708
raise translated_exception
@@ -763,6 +777,7 @@ def verify!
763777
@raw_connection = @unconfigured_connection
764778
@unconfigured_connection = nil
765779
configure_connection
780+
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
766781
@verified = true
767782
return
768783
end
@@ -992,6 +1007,9 @@ def with_raw_connection(allow_retry: false, materialize_transactions: true)
9921007
if @verified
9931008
# Cool, we're confident the connection's ready to use. (Note this might have
9941009
# become true during the above #materialize_transactions.)
1010+
elsif (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
1011+
# We haven't actually verified the connection since we acquired it, but it
1012+
# has been used very recently. We're going to assume it's still okay.
9951013
elsif reconnectable
9961014
if allow_retry
9971015
# Not sure about the connection yet, but if anything goes wrong we can
@@ -1033,6 +1051,7 @@ def with_raw_connection(allow_retry: false, materialize_transactions: true)
10331051
# Barring a known-retryable error inside the query (regardless of
10341052
# whether we were in a _position_ to retry it), we should infer that
10351053
# there's likely a real problem with the connection.
1054+
@last_activity = nil
10361055
@verified = false
10371056
end
10381057

@@ -1047,6 +1066,7 @@ def with_raw_connection(allow_retry: false, materialize_transactions: true)
10471066
# `with_raw_connection` block only when the block is guaranteed to
10481067
# exercise the raw connection.
10491068
def verified!
1069+
@last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
10501070
@verified = true
10511071
end
10521072

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@ def connected?
106106
end
107107

108108
def active?
109-
connected? && @lock.synchronize { @raw_connection&.ping } || false
109+
if connected?
110+
@lock.synchronize do
111+
if @raw_connection&.ping
112+
verified!
113+
true
114+
end
115+
end
116+
end || false
110117
end
111118

112119
alias :reset! :reconnect!

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def active?
349349
@lock.synchronize do
350350
return false unless @raw_connection
351351
@raw_connection.query ";"
352+
verified!
352353
end
353354
true
354355
rescue PG::Error

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,12 @@ def connected?
207207
!(@raw_connection.nil? || @raw_connection.closed?)
208208
end
209209

210-
alias_method :active?, :connected?
210+
def active?
211+
if connected?
212+
verified!
213+
true
214+
end
215+
end
211216

212217
alias :reset! :reconnect!
213218

activerecord/lib/active_record/connection_adapters/trilogy_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def connected?
121121
end
122122

123123
def active?
124-
connected? && @lock.synchronize { @raw_connection&.ping } || false
124+
connected? && @lock.synchronize { @raw_connection&.ping; verified! } || false
125125
rescue ::Trilogy::Error
126126
false
127127
end

activerecord/test/cases/adapter_test.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,11 +611,14 @@ def teardown
611611
assert_predicate @connection, :active?
612612
end
613613

614-
test "querying a 'clean' failed connection restores and succeeds" do
614+
test "querying a 'clean' long-failed connection restores and succeeds" do
615615
remote_disconnect @connection
616616

617617
@connection.clean! # this simulates a fresh checkout from the pool
618618

619+
# Backdate last activity to simulate a connection we haven't used in a while
620+
@connection.instance_variable_set(:@last_activity, Process.clock_gettime(Process::CLOCK_MONOTONIC) - 5.minutes)
621+
619622
# Clean did not verify / fix the connection
620623
assert_not_predicate @connection, :active?
621624

@@ -627,11 +630,30 @@ def teardown
627630
assert_predicate @connection, :active?
628631
end
629632

633+
test "querying a 'clean' recently-used but now-failed connection skips verification" do
634+
remote_disconnect @connection
635+
636+
@connection.clean! # this simulates a fresh checkout from the pool
637+
638+
# Clean did not verify / fix the connection
639+
assert_not_predicate @connection, :active?
640+
641+
# Because the query cannot be retried, and we (mistakenly) believe the
642+
# connection is still good, the query will fail. This is what we want,
643+
# because the alternative would be excessive reverification.
644+
assert_raises(ActiveRecord::AdapterError) do
645+
Post.delete_all
646+
end
647+
end
648+
630649
test "quoting a string on a 'clean' failed connection will not prevent reconnecting" do
631650
remote_disconnect @connection
632651

633652
@connection.clean! # this simulates a fresh checkout from the pool
634653

654+
# Backdate last activity to simulate a connection we haven't used in a while
655+
@connection.instance_variable_set(:@last_activity, Process.clock_gettime(Process::CLOCK_MONOTONIC) - 5.minutes)
656+
635657
# Clean did not verify / fix the connection
636658
assert_not_predicate @connection, :active?
637659

0 commit comments

Comments
 (0)