Skip to content

Commit a180823

Browse files
Prevent marking broken connections as verified
Related to rails#49802 Prior to this commit `with_raw_connection` would always mark the connection as `@verified = true` after yielding it. But yielding a connection is not enough to determine that a connection is healthy. Marking a broken connection as verified can prevent further reconnects, leaving us with a broken connection for longer than expected. For example, * Connection is broken and not verified * Quote string succeeds, and marks the broken connection as verified * Because the connection is verified, querying will not trigger a reconnect * Query fails and the connection is no longer verified * Repeat ad infinitum (or until we reach a query with `allow_retry: true`, or one that isn't preceded by a call to quote string) This commit solves the problem by only marking the connection as verified if we have definitively exercised that connection. This is safer, but it does put more responsibility on each individual adapter. If a call to `verified!` is missing things should still work OK, but we'll end up pinging the connection more to see if it is active.
1 parent 9c85851 commit a180823

File tree

7 files changed

+57
-10
lines changed

7 files changed

+57
-10
lines changed

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,11 @@ def reconnect_can_restore_state?
977977
# If +allow_retry+ is true, a connection-related exception will
978978
# cause an automatic reconnect and re-run of the block, up to
979979
# the connection's configured +connection_retries+ setting
980-
# and the configured +retry_deadline+ limit.
980+
# and the configured +retry_deadline+ limit. (Note that when
981+
# +allow_retry+ is true, it's possible to return without having marked
982+
# the connection as verified. If the block is guaranteed to exercise the
983+
# connection, consider calling `verified!` to avoid needless
984+
# verification queries in subsequent calls.)
981985
#
982986
# If +materialize_transactions+ is false, the block will be run without
983987
# ensuring virtual transactions have been materialized in the DB
@@ -1028,9 +1032,7 @@ def with_raw_connection(allow_retry: false, materialize_transactions: true)
10281032
end
10291033

10301034
begin
1031-
result = yield @raw_connection
1032-
@verified = true
1033-
result
1035+
yield @raw_connection
10341036
rescue => original_exception
10351037
translated_exception = translate_exception_class(original_exception, nil, nil)
10361038
invalidate_transaction(translated_exception)
@@ -1065,6 +1067,13 @@ def with_raw_connection(allow_retry: false, materialize_transactions: true)
10651067
end
10661068
end
10671069

1070+
# Mark the connection as verified. Call this inside a
1071+
# `with_raw_connection` block only when the block is guaranteed to
1072+
# exercise the raw connection.
1073+
def verified!
1074+
@verified = true
1075+
end
1076+
10681077
def retryable_connection_error?(exception)
10691078
exception.is_a?(ConnectionNotEstablished) || exception.is_a?(ConnectionFailed)
10701079
end

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transac
102102
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
103103
sync_timezone_changes(conn)
104104
result = conn.query(sql)
105+
verified!
105106
handle_warnings(sql)
106107
result
107108
end
@@ -130,6 +131,8 @@ def exec_stmt_and_free(sql, name, binds, cache_stmt: false, async: false)
130131
result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
131132
stmt.execute(*type_casted_binds)
132133
end
134+
verified!
135+
result
133136
rescue ::Mysql2::Error => e
134137
if cache_stmt
135138
@statements.delete(sql)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ def query(sql, name = nil) # :nodoc:
1616

1717
log(sql, name) do
1818
with_raw_connection do |conn|
19-
conn.async_exec(sql).map_types!(@type_map_for_results).values
19+
result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20+
verified!
21+
result
2022
end
2123
end
2224
end
@@ -51,6 +53,7 @@ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transac
5153
log(sql, name, async: async) do
5254
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
5355
result = conn.async_exec(sql)
56+
verified!
5457
handle_warnings(result)
5558
result
5659
end

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,9 @@ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactio
893893
type_casted_binds = type_casted_binds(binds)
894894
log(sql, name, binds, type_casted_binds, async: async) do
895895
with_raw_connection do |conn|
896-
conn.exec_params(sql, type_casted_binds)
896+
result = conn.exec_params(sql, type_casted_binds)
897+
verified!
898+
result
897899
end
898900
end
899901
end
@@ -908,7 +910,9 @@ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:
908910
type_casted_binds = type_casted_binds(binds)
909911

910912
log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
911-
conn.exec_prepared(stmt_key, type_casted_binds)
913+
result = conn.exec_prepared(stmt_key, type_casted_binds)
914+
verified!
915+
result
912916
end
913917
end
914918
rescue ActiveRecord::StatementInvalid => e

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: fals
5050
stmt.bind_params(type_casted_binds)
5151
records = stmt.to_a
5252
end
53+
verified!
5354

5455
build_result(columns: cols, rows: records)
5556
end
@@ -76,7 +77,9 @@ def begin_isolated_db_transaction(isolation) # :nodoc:
7677
def begin_db_transaction # :nodoc:
7778
log("begin transaction", "TRANSACTION") do
7879
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
79-
conn.transaction
80+
result = conn.transaction
81+
verified!
82+
result
8083
end
8184
end
8285
end
@@ -112,7 +115,9 @@ def high_precision_current_timestamp
112115
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
113116
log(sql, name, async: async) do
114117
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
115-
conn.execute(sql)
118+
result = conn.execute(sql)
119+
verified!
120+
result
116121
end
117122
end
118123
end
@@ -133,7 +138,9 @@ def execute_batch(statements, name = nil)
133138

134139
log(sql, name) do
135140
with_raw_connection do |conn|
136-
conn.execute_batch2(sql)
141+
result = conn.execute_batch2(sql)
142+
verified!
143+
result
137144
end
138145
end
139146
end

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transac
4747
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
4848
sync_timezone_changes(conn)
4949
result = conn.query(sql)
50+
verified!
5051
handle_warnings(sql)
5152
result
5253
end

activerecord/test/cases/adapter_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,26 @@ def teardown
612612
assert_predicate @connection, :active?
613613
end
614614

615+
test "quoting a string on a 'clean' failed connection will not prevent reconnecting" do
616+
remote_disconnect @connection
617+
618+
@connection.clean! # this simulates a fresh checkout from the pool
619+
620+
# Clean did not verify / fix the connection
621+
assert_not_predicate @connection, :active?
622+
623+
# Quote string will not verify a broken connection (although it may
624+
# reconnect in some cases)
625+
Post.connection.quote_string("")
626+
627+
# Because the connection hasn't been verified since checkout,
628+
# and the query cannot safely be retried, the connection will be
629+
# verified before querying.
630+
Post.delete_all
631+
632+
assert_predicate @connection, :active?
633+
end
634+
615635
test "querying after a failed query restores and succeeds" do
616636
Post.first # Connection verified (and prepared statement pool populated if enabled)
617637

0 commit comments

Comments
 (0)