Skip to content

Commit 530d771

Browse files
authored
Merge pull request rails#51336 from adrianna-chang-shopify/ac-retry-idempotent-queries-2
Retry known idempotent SELECT queries on connection-related exceptions
2 parents a2d2155 + eabcff2 commit 530d771

31 files changed

+221
-52
lines changed

activerecord/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Retry known idempotent SELECT queries on connection-related exceptions
2+
3+
SELECT queries we construct by walking the Arel tree and / or with known model attributes
4+
are idempotent and can safely be retried in the case of a connection error. Previously,
5+
adapters such as `TrilogyAdapter` would raise `ActiveRecord::ConnectionFailed: Trilogy::EOFError`
6+
when encountering a connection error mid-request.
7+
8+
*Adrianna Chang*
9+
110
* Allow association's `foreign_key` to be composite.
211

312
`query_constraints` option was the only way to configure a composite foreign key by passing an `Array`.

activerecord/lib/active_record/associations/join_dependency/join_association.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def strict_loading?
9393
def append_constraints(connection, join, constraints)
9494
if join.is_a?(Arel::Nodes::StringJoin)
9595
join_string = Arel::Nodes::And.new(constraints.unshift join.left)
96-
join.left = Arel.sql(connection.visitor.compile(join_string))
96+
join.left = Arel.sql(connection.visitor.compile(join_string), retryable: true)
9797
else
9898
right = join.right
9999
right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def to_sql(arel_or_sql_string, binds = [])
1414
sql
1515
end
1616

17-
def to_sql_and_binds(arel_or_sql_string, binds = [], preparable = nil) # :nodoc:
17+
def to_sql_and_binds(arel_or_sql_string, binds = [], preparable = nil, allow_retry = false) # :nodoc:
1818
# Arel::TreeManager -> Arel::Node
1919
if arel_or_sql_string.respond_to?(:ast)
2020
arel_or_sql_string = arel_or_sql_string.ast
@@ -27,6 +27,7 @@ def to_sql_and_binds(arel_or_sql_string, binds = [], preparable = nil) # :nodoc:
2727
end
2828

2929
collector = collector()
30+
collector.retryable = true
3031

3132
if prepared_statements
3233
collector.preparable = true
@@ -41,10 +42,11 @@ def to_sql_and_binds(arel_or_sql_string, binds = [], preparable = nil) # :nodoc:
4142
else
4243
sql = visitor.compile(arel_or_sql_string, collector)
4344
end
44-
[sql.freeze, binds, preparable]
45+
allow_retry = collector.retryable
46+
[sql.freeze, binds, preparable, allow_retry]
4547
else
4648
arel_or_sql_string = arel_or_sql_string.dup.freeze unless arel_or_sql_string.frozen?
47-
[arel_or_sql_string, binds, preparable]
49+
[arel_or_sql_string, binds, preparable, allow_retry]
4850
end
4951
end
5052
private :to_sql_and_binds
@@ -64,11 +66,15 @@ def cacheable_query(klass, arel) # :nodoc:
6466
end
6567

6668
# Returns an ActiveRecord::Result instance.
67-
def select_all(arel, name = nil, binds = [], preparable: nil, async: false)
69+
def select_all(arel, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
6870
arel = arel_from_relation(arel)
69-
sql, binds, preparable = to_sql_and_binds(arel, binds, preparable)
71+
sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable, allow_retry)
7072

71-
select(sql, name, binds, prepare: prepared_statements && preparable, async: async && FutureResult::SelectAll)
73+
select(sql, name, binds,
74+
prepare: prepared_statements && preparable,
75+
async: async && FutureResult::SelectAll,
76+
allow_retry: allow_retry
77+
)
7278
rescue ::RangeError
7379
ActiveRecord::Result.empty(async: async)
7480
end
@@ -495,7 +501,7 @@ def with_yaml_fallback(value) # :nodoc:
495501
end
496502

497503
# This is a safe default, even if not high precision on all databases
498-
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc:
504+
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP", retryable: true).freeze # :nodoc:
499505
private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
500506

501507
# Returns an Arel SQL literal for the CURRENT_TIMESTAMP for usage with
@@ -507,7 +513,7 @@ def high_precision_current_timestamp
507513
HIGH_PRECISION_CURRENT_TIMESTAMP
508514
end
509515

510-
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
516+
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
511517
raise NotImplementedError
512518
end
513519

@@ -606,7 +612,7 @@ def combine_multi_statements(total_sql)
606612
end
607613

608614
# Returns an ActiveRecord::Result instance.
609-
def select(sql, name = nil, binds = [], prepare: false, async: false)
615+
def select(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false)
610616
if async && async_enabled?
611617
if current_transaction.joinable?
612618
raise AsynchronousQueryInsideTransactionError, "Asynchronous queries are not allowed inside transactions"
@@ -627,7 +633,7 @@ def select(sql, name = nil, binds = [], prepare: false, async: false)
627633
return future_result
628634
end
629635

630-
result = internal_exec_query(sql, name, binds, prepare: prepare)
636+
result = internal_exec_query(sql, name, binds, prepare: prepare, allow_retry: allow_retry)
631637
if async
632638
FutureResult.wrap(result)
633639
else

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,19 @@ def clear_query_cache
204204
pool.clear_query_cache
205205
end
206206

207-
def select_all(arel, name = nil, binds = [], preparable: nil, async: false) # :nodoc:
207+
def select_all(arel, name = nil, binds = [], preparable: nil, async: false, allow_retry: false) # :nodoc:
208208
arel = arel_from_relation(arel)
209209

210210
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch.
211211
# Such queries should not be cached.
212212
if @query_cache&.enabled? && !(arel.respond_to?(:locked) && arel.locked)
213-
sql, binds, preparable = to_sql_and_binds(arel, binds, preparable)
213+
sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable)
214214

215215
if async
216-
result = lookup_sql_cache(sql, name, binds) || super(sql, name, binds, preparable: preparable, async: async)
216+
result = lookup_sql_cache(sql, name, binds) || super(sql, name, binds, preparable: preparable, async: async, allow_retry: allow_retry)
217217
FutureResult.wrap(result)
218218
else
219-
cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable, async: async) }
219+
cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable, async: async, allow_retry: allow_retry) }
220220
end
221221
else
222222
super

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,12 @@ def disable_referential_integrity # :nodoc:
229229
# Mysql2Adapter doesn't have to free a result after using it, but we use this method
230230
# to write stuff in an abstract way without concerning ourselves about whether it
231231
# needs to be explicitly freed or not.
232-
def execute_and_free(sql, name = nil, async: false) # :nodoc:
232+
def execute_and_free(sql, name = nil, async: false, allow_retry: false) # :nodoc:
233233
sql = transform_query(sql)
234234
check_if_write_query(sql)
235235

236236
mark_transaction_written_if_write(sql)
237-
yield raw_execute(sql, name, async: async)
237+
yield raw_execute(sql, name, async: async, allow_retry: allow_retry)
238238
end
239239

240240
def begin_db_transaction # :nodoc:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module DatabaseStatements
1111

1212
# https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_current-timestamp
1313
# https://dev.mysql.com/doc/refman/5.7/en/date-and-time-type-syntax.html
14-
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)").freeze # :nodoc:
14+
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)", retryable: true).freeze # :nodoc:
1515
private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
1616

1717
def write_query?(sql) # :nodoc:

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def select_all(*, **) # :nodoc:
1818
result
1919
end
2020

21-
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
21+
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
2222
if without_prepared_statement?(binds)
23-
execute_and_free(sql, name, async: async) do |result|
23+
execute_and_free(sql, name, async: async, allow_retry: allow_retry) do |result|
2424
if result
2525
build_result(columns: result.fields, rows: result.to_a)
2626
else

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def exec_restart_db_transaction # :nodoc:
124124
end
125125

126126
# From https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
127-
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc:
127+
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP", retryable: true).freeze # :nodoc:
128128
private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
129129

130130
def high_precision_current_timestamp

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ def exec_no_cache(sql, name, binds, async:, allow_retry:, materialize_transactio
881881

882882
type_casted_binds = type_casted_binds(binds)
883883
log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
884-
with_raw_connection(allow_retry: false, materialize_transactions: materialize_transactions) do |conn|
884+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
885885
result = conn.exec_params(sql, type_casted_binds)
886886
verified!
887887
notification_payload[:row_count] = result.count
@@ -895,7 +895,7 @@ def exec_cache(sql, name, binds, async:, allow_retry:, materialize_transactions:
895895

896896
update_typemap_for_default_timezone
897897

898-
with_raw_connection(allow_retry: false, materialize_transactions: materialize_transactions) do |conn|
898+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
899899
stmt_key = prepare_statement(sql, binds, conn)
900900
type_casted_binds = type_casted_binds(binds)
901901

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def explain(arel, binds = [], _options = [])
2121
SQLite3::ExplainPrettyPrinter.new.pp(result)
2222
end
2323

24-
def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: false) # :nodoc:
24+
def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
2525
sql = transform_query(sql)
2626
check_if_write_query(sql)
2727

@@ -106,7 +106,7 @@ def exec_rollback_db_transaction # :nodoc:
106106

107107
# https://stackoverflow.com/questions/17574784
108108
# https://www.sqlite.org/lang_datefunc.html
109-
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')").freeze # :nodoc:
109+
HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')", retryable: true).freeze # :nodoc:
110110
private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
111111

112112
def high_precision_current_timestamp

0 commit comments

Comments
 (0)