Skip to content

Commit 6c70d02

Browse files
authored
Merge pull request rails#46046 from adrianna-chang-shopify/ac-db-retries-with-timeout
Take into account timeout limit when retrying queries
2 parents 8354c3a + 4d13f05 commit 6c70d02

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

activerecord/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Allow applications to set retry deadline for query retries.
2+
3+
Building on the work done in #44576 and #44591, we extend the logic that automatically
4+
reconnects database connections to take into account a timeout limit. We won't retry
5+
a query if a given amount of time has elapsed since the query was first attempted. This
6+
value defaults to nil, meaning that all retryable queries are retried regardless of time elapsed,
7+
but this can be changed via the `retry_deadline` option in the database config.
8+
9+
*Adrianna Chang*
10+
111
* Fix a case where the query cache can return wrong values. See #46044
212

313
*Aaron Patterson*

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ def connection_retries
169169
(@config[:connection_retries] || 1).to_i
170170
end
171171

172+
def retry_deadline
173+
if @config[:retry_deadline]
174+
@config[:retry_deadline].to_f
175+
else
176+
nil
177+
end
178+
end
179+
172180
def default_timezone
173181
@default_timezone || ActiveRecord.default_timezone
174182
end
@@ -582,6 +590,7 @@ def active?
582590
# instead.
583591
def reconnect!(restore_transactions: false)
584592
retries_available = connection_retries
593+
deadline = retry_deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) + retry_deadline
585594

586595
@lock.synchronize do
587596
reconnect
@@ -596,8 +605,9 @@ def reconnect!(restore_transactions: false)
596605
end
597606
rescue => original_exception
598607
translated_exception = translate_exception_class(original_exception, nil, nil)
608+
retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC)
599609

600-
if retries_available > 0
610+
if !retry_deadline_exceeded && retries_available > 0
601611
retries_available -= 1
602612

603613
if retryable_connection_error?(translated_exception)
@@ -872,7 +882,8 @@ def reconnect_can_restore_state?
872882
#
873883
# If +allow_retry+ is true, a connection-related exception will
874884
# cause an automatic reconnect and re-run of the block, up to
875-
# the connection's configured +connection_retries+ setting.
885+
# the connection's configured +connection_retries+ setting
886+
# and the configured +retry_deadline+ limit.
876887
#
877888
# If +uses_transaction+ is false, the block will be run without
878889
# ensuring virtual transactions have been materialized in the DB
@@ -901,6 +912,7 @@ def with_raw_connection(allow_retry: false, uses_transaction: true)
901912
materialize_transactions if uses_transaction
902913

903914
retries_available = allow_retry ? connection_retries : 0
915+
deadline = retry_deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) + retry_deadline
904916
reconnectable = reconnect_can_restore_state?
905917

906918
if @verified
@@ -927,8 +939,9 @@ def with_raw_connection(allow_retry: false, uses_transaction: true)
927939
result
928940
rescue => original_exception
929941
translated_exception = translate_exception_class(original_exception, nil, nil)
942+
retry_deadline_exceeded = deadline && deadline < Process.clock_gettime(Process::CLOCK_MONOTONIC)
930943

931-
if retries_available > 0
944+
if !retry_deadline_exceeded && retries_available > 0
932945
retries_available -= 1
933946

934947
if retryable_query_error?(translated_exception)

activerecord/test/cases/adapter_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,45 @@ def teardown
651651
assert_operator Post.count, :>, 0
652652
end
653653

654+
test "can reconnect and retry queries under limit when retry deadline is set" do
655+
attempts = 0
656+
@connection.stub(:retry_deadline, 0.1) do
657+
@connection.send(:with_raw_connection, allow_retry: true) do
658+
if attempts == 0
659+
attempts += 1
660+
raise ActiveRecord::ConnectionFailed.new("Something happened to the connection")
661+
end
662+
end
663+
end
664+
end
665+
666+
test "does not reconnect and retry queries when retries are disabled" do
667+
assert_raises(ActiveRecord::ConnectionFailed) do
668+
attempts = 0
669+
@connection.send(:with_raw_connection) do
670+
if attempts == 0
671+
attempts += 1
672+
raise ActiveRecord::ConnectionFailed.new("Something happened to the connection")
673+
end
674+
end
675+
end
676+
end
677+
678+
test "does not reconnect and retry queries that exceed retry deadline" do
679+
assert_raises(ActiveRecord::ConnectionFailed) do
680+
attempts = 0
681+
@connection.stub(:retry_deadline, 0.1) do
682+
@connection.send(:with_raw_connection, allow_retry: true) do
683+
if attempts == 0
684+
sleep(0.2)
685+
attempts += 1
686+
raise ActiveRecord::ConnectionFailed.new("Something happened to the connection")
687+
end
688+
end
689+
end
690+
end
691+
end
692+
654693
private
655694
def raw_transaction_open?(connection)
656695
case connection.class::ADAPTER_NAME

guides/source/configuring.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,6 +2822,33 @@ development:
28222822
use_metadata_table: false
28232823
```
28242824
2825+
#### Configuring Retry Behaviour
2826+
2827+
By default, Rails will automatically reconnect to the database server and retry certain queries
2828+
if something goes wrong. Only safely retryable (idempotent) queries will be retried. The number
2829+
of retries can be specified in your the database configuration via `connection_retries`, or disabled
2830+
by setting the value to 0. The default number of retries is 1.
2831+
2832+
```yaml
2833+
development:
2834+
adapter: mysql2
2835+
connection_retries: 3
2836+
```
2837+
2838+
The database config also allows a `retry_deadline` to be configured. If a `retry_deadline` is configured,
2839+
an otherwise-retryable query will _not_ be retried if the specified time has elapsed while the query was
2840+
first tried. For example, a `retry_deadline` of 5 seconds means that if 5 seconds have passed since a query
2841+
was first attempted, we won't retry the query, even if it is idempotent and there are `connection_retries` left.
2842+
2843+
This value defaults to nil, meaning that all retryable queries are retried regardless of time elapsed.
2844+
The value for this config should be specified in seconds.
2845+
2846+
```yaml
2847+
development:
2848+
adapter: mysql2
2849+
retry_deadline: 5 # Stop retrying queries after 5 seconds
2850+
```
2851+
28252852
### Creating Rails Environments
28262853

28272854
By default Rails ships with three environments: "development", "test", and "production". While these are sufficient for most use cases, there are circumstances when you want more environments.

0 commit comments

Comments
 (0)