Retriable is an simple DSL to retry failed code blocks with randomized exponential backoff.
Retriable is an simple DSL to retry failed code blocks with randomized exponential backoff.
Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff time intervals. This is especially useful when interacting external APIs, remote services, or file system calls.
Ruby 3.2+
If you need Ruby 2.3.0-3.1.x support, use the 3.8.x branch by specifying ~> 3.8 in your Gemfile.
If you need Ruby 2.0.0-2.2.x support, use the 3.1 branch by specifying ~3.1 in your Gemfile.
If you need Ruby 1.9.3 support, use the 2.x branch by specifying ~2.1 in your Gemfile.
If you need Ruby 1.8.x to 1.9.2 support, use the 1.x branch by specifying ~1.4 in your Gemfile.
Retriable 5.0 changes one thing for existing users: Retriable.config is now a
frozen snapshot, so direct writes raise FrozenError.
Retriable.config.sleep_disabled = true # => FrozenError
Retriable.configure { |c| c.sleep_disabled = true } # do this insteadTest setup files such as spec_helper and rails_helper are the likeliest
place this bites. For the full 4.x to 5.x and 3.x to 4.0 guides, see
docs/migration.md.
Via command line:
gem install retriableIn your ruby script:
require 'retriable'In your Gemfile:
gem 'retriable', '~> 5.0'Code in a Retriable.retriable block will be retried if an exception is raised.
require 'retriable'
class Api
# Use it in methods that interact with unreliable services
def get
Retriable.retriable do
# code here...
end
end
endBy default, Retriable will:
StandardErrorThe default interval table with 10 tries looks like this (in seconds, rounded to the nearest millisecond):
| Retry # | Min | Average | Max |
|---|---|---|---|
| 1 | 0.25 |
0.5 |
0.75 |
| 2 | 0.375 |
0.75 |
1.125 |
| 3 | 0.563 |
1.125 |
1.688 |
| 4 | 0.844 |
1.688 |
2.531 |
| 5 | 1.266 |
2.531 |
3.797 |
| 6 | 1.898 |
3.797 |
5.695 |
| 7 | 2.848 |
5.695 |
8.543 |
| 8 | 4.271 |
8.543 |
12.814 |
| 9 | 6.407 |
12.814 |
19.222 |
| 10 | stop | stop | stop |
Here are the available options, in some vague order of relevance to most common use patterns:
| Option | Default | Definition |
|---|---|---|
tries |
3 |
Number of attempts to make at running your code block (includes initial attempt). Pass Float::INFINITY to keep retrying until success or until max_elapsed_time is reached. |
on |
[StandardError] |
Type of exceptions to retry. Read more. |
retry_if |
nil |
Callable (for example a Proc or lambda) that receives the rescued exception and returns true/false to decide whether to retry. Read more. |
on_retry |
nil |
Proc to call after each try is rescued. Pass false to disable a callback set in #configure for a single call. Read more. |
on_give_up |
nil |
Proc to call when Retriable stops retrying after a rescued retriable exception. Read more. |
sleep_disabled |
false |
When true, disable exponential backoff and attempt retries immediately. |
base_interval |
0.5 |
The initial interval in seconds between tries. |
max_elapsed_time |
900 (15 min) |
The maximum amount of total time in seconds that code is allowed to keep being retried. Set to nil to disable the time limit and retry based solely on tries. |
max_interval |
60 |
Caps the interval before rand_factor is applied. A randomized interval may reach max_interval * (1 + rand_factor). |
multiplier |
1.5 |
Each successive interval grows by this factor. A multipler of 1.5 means the next interval will be 1.5x the current interval. |
rand_factor |
0.5 |
The percentage to randomize the next retry interval time. The next interval calculation is randomized_interval = retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor]) |
intervals |
nil |
Skip generated intervals and provide your own array of intervals in seconds. Read more. |
Timing options are validated before retrying. tries must be a positive integer when Retriable generates intervals, or Float::INFINITY for unbounded retries. base_interval, max_interval, multiplier, and max_elapsed_time must be non-negative numbers, with max_elapsed_time also accepting nil. rand_factor must be a number from 0 through 1. If provided, intervals must be an array of non-negative numbers; because it replaces generated intervals, it also overrides tries, base_interval, max_interval, rand_factor, and multiplier validation. intervals cannot be combined with tries: Float::INFINITY.
:on Can take the form:
Exception class (retry every exception of this type, including subclasses)Array or Set of Exception classes (retry any exception of one of these types, including subclasses)Hash where the keys are Exception classes and the values are one of:nil (retry every exception of the key's type, including subclasses)Regexp pattern (retries exceptions ONLY if their message matches the pattern)message matches at least one of the patterns)Use :retry_if when retry logic depends on details that :on does not cover. The Proc receives the rescued exception and should return true to retry or false to re-raise immediately.
def caused_by?(error, klass)
current = error
while current
return true if current.is_a?(klass)
current = current.cause
end
false
end
Retriable.retriable(
on: [Faraday::ConnectionFailed],
retry_if: ->(exception) { caused_by?(exception, Errno::ECONNRESET) }
) do
# code here...
end:retry_if runs after the exception type has matched :on.
You can change the global defaults with a #configure block:
Retriable.configure do |c|
c.tries = 5
c.max_elapsed_time = 3600 # 1 hour
end#configure sets defaults only. Per-call options passed to Retriable.retriable and
Retriable.with_context still take precedence.
When a higher-precedence layer sets tries: without intervals:, it clears any
intervals: inherited from a lower layer (so retriable(tries: 1) runs once even
if intervals was configured). Within a single call, passing intervals: still
overrides tries:.
#configure is the only supported way to change configuration, and it is safe to
call from multiple threads.
Configuration is copy-on-write. #configure duplicates the current config, hands
your block the copy, and publishes it only if the block returns without raising.
So a reader in another thread always sees either the whole previous config or the
whole new one, never a half-applied mix, and a block that raises leaves the
existing config in place.
Configuration blocks are serialized. Keep them short, and do not wait inside one
for work that may call #configure, because that work cannot begin until the
current block returns. Readers are unaffected and continue using the last
published config while a block runs.
The published config is deeply frozen. Reaching around #configure to mutate it
raises FrozenError:
Retriable.config.tries = 5 # => FrozenError
Retriable.config.contexts[:api] = {} # => FrozenError
Retriable.configure { |c| c.tries = 5 } # this is the supported pathThat is deliberate. A published config is shared by every thread reading it, so an
in-place write is a data race that used to corrupt other threads' retry behavior
silently. Retriable.config remains fine to read.
Two more details:
#configure calls can nest. Nested calls on the configuring thread, including
calls from its fibers, share the outer working copy. Only the outermost call
publishes. If its block raises, none of the nested changes are published. A
nested call does not create an independent commit or savepoint.#configure block, every fiber on the configuring thread sees the
in-progress config. Other threads keep seeing the last published one until the
block completes.Thread safety covers the config structure. User-supplied callbacks such as
retry_if, on_retry, and on_give_up can still hold mutable state. The caller
must synchronize that state if the callback can run from multiple threads.
#with_override is a block-scoped API for forcing retry options that should
take precedence over both `#c
No open issues yet, or sync has not completed.