Project

reattempt

0.0
No commit activity in last 3 years
No release in over 3 years
Enumerable APIs for retries and exponential backoff with jitter
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.16
~> 5.0
~> 10.0

Runtime

 Project Readme

Reattempt

Simple Enumerable APIs offering retries with exponential backoff and jitter.

Installation

Add this line to your application's Gemfile:

gem 'reattempt'

And then execute:

$ bundle

Or install it yourself as:

$ gem install reattempt

Synopsis

Simplest use with the defaults - 5 attempts, 0.02 to 1 second delay, 0.2 jitter (delay is randomised ±10%), catching StandardError:

begin
  Reattempt::Retry.new.each do
    poke_remote_api
  end
rescue Reattempt::RetriesExceeded => e
  handle_repeated_failure(e.cause)
end

Instances are thread-safe, and it's suggested that you separate their creation from usage: inject them as dependencies, configure them in class attributes, store them in constants, etc.

Usage

Reattempt consists of two main classes:

Backoff

Backoff implements a simple jittered exponential backoff calculator as an Enumerable:

# Start delay 0.075-0.125 seconds, increasing to 0.75-1.25 seconds
bo = Reattempt::Backoff.new(min_delay: 0.1, max_delay: 1.0, jitter: 0.5)

bo.take(4).map { |x| x.round(4) } # => [0.1138, 0.2029, 0.4227, 0.646]
bo.take(2).each { |delay| sleep delay }
bo.delay_for_attempt(4) # => 1.0403524624141058
bo[4] # => 0.8328055668923606

bo.each do |delay|
  printf("Sleeping for about %.2f seconds\n", delay)
  sleep delay
end

Note Backoff is strictly a calculator, it does not implement sleep itself.

The iterator is unbounded and you're expected to take however many you need, or manually exit the loop.

Retry

Retry implements a retrying iterator, catching the given Exception types and sleeping as per a configured Backoff instance.

bo = Reattempt::Backoff.new(min_delay: 0.1, max_delay: 1.0, jitter: 0.5)
try = Reattempt::Retry.new(tries: 5, rescue: TempError, backoff: bo)
begin
  try.each do |attempt|
    raise TempError, "Failed in attempt #{attempt}"
  end
rescue Reattempt::RetriesExceeded => e
  p e.cause # => #<TempError: "Failed in attempt 5">
end

rescue is coerced to an array, and its contents are simply expected to respond to ===, so you can do complex matching like this:

exception_matcher = ->(ex) do
  ex.is_a?(IOError) && ex.message.includes?('closed stream')
end

try = Reattempt::Retry.new(rescue: [exception_matcher, SomeOtherException])

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Freaky/ruby-reattempt.

License

The gem is available as open source under the terms of the MIT License.