retriable-3.0.1/0000755000175000017500000000000013054002021012473 5ustar pravipraviretriable-3.0.1/CHANGELOG.md0000644000175000017500000001044613054002021014311 0ustar pravipravi## HEAD ## 3.0.1 * Add `rubocop` linter to enforce coding styles for this library. Also, fix rule violations. * Removed `attr_reader :config` that caused a warning. @bruno- * Clean up Rakefile testing cruft. @bruno- * Use `.any?` in the `:on` hash processing. @apurvis ## 3.0.0 * Require ruby 2.0+. * Breaking Change: `on` with a `Hash` value now matches subclassed exceptions. Thanks @apurvis! * Remove `awesome_print` from development environment. ## 2.1.0 * Fix bug #17 due to confusing the initial try as a retry. * Switch to `Minitest` 5.6 expect syntax. ## 2.0.2 * Change required_ruby_version in gemspec to >= 1.9.3. ## 2.0.1 * Add support for ruby 1.9.3. ## 2.0.0 * Require ruby 2.0+. * Time intervals default to randomized exponential backoff instead of fixed time intervals. The delay between retries grows with every attempt and there's a randomization factor added to each attempt. * `base_interval`, `max_interval`, `rand_factor`, and `multiplier` are new arguments that are used to generate randomized exponential back off time intervals. * `interval` argument removed. * Accept `intervals` array argument to provide your own custom intervals. * Allow configurable defaults via `Retriable#configure` block. * Add ability for `:on` argument to accept a `Hash` where the keys are exception types and the values are a single or array of `Regexp` pattern(s) to match against exception messages for retrial. * Raise, not return, on max elapsed time. * Check for elapsed time after next interval is calculated and it goes over the max elapsed time. * Support early termination via `max_elapsed_time` argument. ## 2.0.0.beta5 * Change `:max_tries` back to `:tries`. ## 2.0.0.beta4 * Change #retry back to #retriable. Didn't like the idea of defining a method that is also a reserved word. * Add ability for `:on` argument to accept a `Hash` where the keys are exception types and the values are a single or array of `Regexp` pattern(s) to match against exception messages for retrial. ## 2.0.0.beta3 * Accept `intervals` array argument to provide your own custom intervals. * Refactor the exponential backoff code into it's own class. * Add specs for exponential backoff, randomization, and config. ## 2.0.0.beta2 * Raise, not return, on max elapsed time. * Check for elapsed time after next interval is calculated and it goes over the max elapsed time. * Add specs for `max_elapsed_time` and `max_interval`. ## 2.0.0.beta1 * Require ruby 2.0+. * Default to random exponential backoff, removes the `interval` option. Exponential backoff is configurable via arguments. * Allow configurable defaults via `Retriable#configure` block. * Change `Retriable.retriable` to `Retriable.retry`. * Support early termination via `max_elapsed_time` argument. ## 1.4.1 * Fixes non kernel mode bug. Remove DSL class, move `#retriable` into Retriable module. Thanks @mkrogemann. ## 1.4.0 * By default, retriable doesn't monkey patch `Kernel`. If you want this functionality, you can `require 'retriable/core_ext/kernel'. * Upgrade minitest to 5.x. * Refactor the DSL into it's own class. ## 1.3.3.1 * Allow sleep parameter to be a proc/lambda to allow for exponential backoff. ## 1.3.3 * sleep after executing the retry block, so there's no wait on the first call (molfar) ## 1.3.2 * Clean up option defaults. * By default, rescue StandardError and Timeout::Error instead of [Exception](http://www.mikeperham.com/2012/03/03/the-perils-of-rescue-exception). ## 1.3.1 * Add `rake` dependency for travis-ci. * Update gemspec summary and description. ## 1.3.0 * Rewrote a lot of the code with inspiration from [attempt](https://rubygems.org/gems/attempt). * Add timeout option to the code block. * Include in Kernel by default, but allow require 'retriable/no_kernel' to load a non kernel version. * Renamed `:times` option to `:tries`. * Renamed `:sleep` option to `:interval`. * Renamed `:then` option to `:on_retry`. * Removed other callbacks, you can wrap retriable in a begin/rescue/else/ensure block if you need that functionality. It avoids the need to define multiple Procs and makes the code more readable. * Rewrote most of the README ## 1.2.0 * Forked the retryable-rb repo. * Extend the Kernel module with the retriable method so you can use it anywhere without having to include it in every class. * Update gemspec, Gemfile, and Raketask. * Remove echoe dependency. retriable-3.0.1/README.md0000644000175000017500000002334613054002021013762 0ustar pravipravi#Retriable [![Build Status](https://secure.travis-ci.org/kamui/retriable.svg)](http://travis-ci.org/kamui/retriable) [![Code Climate](https://codeclimate.com/github/kamui/retriable/badges/gpa.svg)](https://codeclimate.com/github/kamui/retriable) [![Test Coverage](https://codeclimate.com/github/kamui/retriable/badges/coverage.svg)](https://codeclimate.com/github/kamui/retriable) Retriable is a simple DSL to retry failed code blocks with randomized [exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff) time intervals. This is especially useful when interacting external api/services or file system calls. ## Requirements Ruby 2.0.0+ If you need ruby 1.9.3 support, use the [2.x branch](https://github.com/kamui/retriable/tree/2.x) by specifying `~2.1` in your Gemfile. If you need ruby 1.8.x to 1.9.2 support, use the [1.x branch](https://github.com/kamui/retriable/tree/1.x) by specifying `~1.4` in your Gemfile. ## Installation via command line: ```ruby gem install retriable ``` In your ruby script: ```ruby require 'retriable' ``` In your Gemfile: ```ruby gem 'retriable', '~> 3.0' ``` ## Usage Code in a `Retriable.retriable` block will be retried if an exception is raised. By default, Retriable will rescue any exception inherited from `StandardError`, make 3 tries (including the initial attempt) before raising the last exception, and also use randomized exponential backoff to calculate each succeeding try interval. The default interval table with 10 tries looks like this (in seconds): | retry# | retry interval | randomized interval | | -------- | -------------- | ------------------------------- | | 1 | 0.5 | [0.25, 0.75] | | 2 | 0.75 | [0.375, 1.125] | | 3 | 1.125 | [0.5625, 1.6875] | | 4 | 1.6875 | [0.84375, 2.53125] | | 5 | 2.53125 | [1.265625, 3.796875] | | 6 | 3.796875 | [1.8984375, 5.6953125] | | 7 | 5.6953125 | [2.84765625, 8.54296875] | | 8 | 8.54296875 | [4.271484375, 12.814453125] | | 9 | 12.814453125 | [6.4072265625, 19.2216796875] | | 10 | 19.2216796875 | stop | ```ruby require 'retriable' class Api # Use it in methods that interact with unreliable services def get Retriable.retriable do # code here... end end end ``` ### Options Here are the available options: `tries` (default: 3) - Number of attempts to make at running your code block (includes intial attempt). `base_interval` (default: 0.5) - The initial interval in seconds between tries. `max_interval` (default: 60) - The maximum interval in seconds that any try can reach. `rand_factor` (default: 0.25) - The percent range above and below the next interval is randomized between. The calculation is calculated like this: ``` randomized_interval = retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor]) ``` `multiplier` (default: 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. `max_elapsed_time` (default: 900 (15 min)) - The maximum amount of total time that code is allowed to keep being retried. `intervals` (default: nil) - Skip generated intervals and provide your own array of intervals in seconds. Setting this option will ignore `tries`, `base_interval`, `max_interval`, `rand_factor`, and `multiplier` values. `timeout` (default: nil) - Number of seconds to allow the code block to run before raising a `Timeout::Error` inside each try. Default is `nil` means the code block can run forever without raising error. `on` (default: [StandardError]) - An `Array` of exceptions to rescue for each try, a `Hash` where the keys are `Exception` classes and the values can be a single `Regexp` pattern or a list of patterns, or a single `Exception` type. Subclasses of the listed exceptions will be retried and have their messages matched in the same way. `on_retry` - (default: nil) - Proc to call after each try is rescued. ### Config You can change the global defaults with a `#configure` block: ```ruby Retriable.configure do |c| c.tries = 5 c.max_elapsed_time = 3600 # 1 hour end ``` ### Examples `Retriable.retriable` accepts custom arguments. This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try. ```ruby Retriable.retriable on: Timeout::Error, tries: 3, base_interval: 1 do # code here... end ``` You can also specify multiple errors to retry on by passing an array of exceptions. ```ruby Retriable.retriable on: [Timeout::Error, Errno::ECONNRESET] do # code here... end ``` You can also specify a Hash of exceptions where the values are a list or single Regexp pattern. ```ruby Retriable.retriable on: { ActiveRecord::RecordNotUnique => nil, ActiveRecord::RecordInvalid => [/Email has already been taken/, /Username has already been taken/], Mysql2::Error => /Duplicate entry/ } do # code here... end ``` You can also specify a timeout if you want the code block to only try for X amount of seconds. This timeout is per try. ```ruby Retriable.retriable timeout: 60 do # code here... end ``` If you need millisecond units of time for the sleep or the timeout: ```ruby Retriable.retriable base_interval: (200/1000.0), timeout: (500/1000.0) do # code here... end ``` ### Custom Interval Array You can also bypass the built-in interval generation and provide your own array of intervals. Supplying your own intervals overrides the `tries`, `base_interval`, `max_interval`, `rand_factor`, and `multiplier` parameters. ```ruby Retriable.retriable intervals: [0.5, 1.0, 2.0, 2.5] do # code here... end ``` This example makes 5 total attempts, if the first attempt fails, the 2nd attempt occurs 0.5 seconds later. ### Turn off Exponential Backoff Exponential backoff is enabled by default, if you want to simply retry code every second, 5 times maximum, you can do this: ```ruby Retriable.retriable tries: 5, base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0 do # code here... end ``` This works by starting at a 1 second interval (`base_interval`), setting the `multipler` to 1.0 means each subsequent try will increase 1x, which is still `1.0` seconds, and then a `rand_factor` of 0.0 means that there's no randomization of that interval. By default, it would randomize 0.25 seconds, which would mean normally the intervals would randomize between 0.75 and 1.25 seconds, but in this case `rand_factor` is basically being disabled. Another way to accomplish this would be to create an array with a fixed interval. In this example, `Array.new(5, 1)` creates an array with 5 elements, all with the value 1. The code block will retry up to 5 times, and wait 1 second between each attempt. ```ruby # Array.new(5, 1) # => [1, 1, 1, 1, 1] Retriable.retriable intervals: Array.new(5, 1) do # code here... end ``` If you don't want exponential backoff, but you still want some randomization between intervals, this code will run every 1 seconds with a randomization factor of 0.2, which means each interval will be a random value between 0.8 and 1.2 (1 second +/- 0.2): ```ruby Retriable.retriable base_interval: 1.0, multiplier: 1.0, rand_factor: 0.2 do # code here... end ``` ### Callbacks `#retriable` also provides a callback called `:on_retry` that will run after an exception is rescued. This callback provides the `exception` that was raised in the current try, the `try_number`, the `elapsed_time` for all tries so far, and the time in seconds of the `next_interval`. As these are specified in a `Proc`, unnecessary variables can be left out of the parameter list. ```ruby do_this_on_each_retry = Proc.new do |exception, try, elapsed_time, next_interval| log "#{exception.class}: '#{exception.message}' - #{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try." end Retriable.retriable on_retry: do_this_on_each_retry do # code here... end ``` ### Ensure/Else What if I want to execute a code block at the end, whether or not an exception was rescued ([ensure](http://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-ensure))? Or, what if I want to execute a code block if no exception is raised ([else](http://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-else))? Instead of providing more callbacks, I recommend you just wrap retriable in a begin/retry/else/ensure block: ```ruby begin Retriable.retriable do # some code end rescue => e # run this if retriable ends up re-rasing the exception else # run this if retriable doesn't raise any exceptions ensure # run this no matter what, exception or no exception end ``` ## Kernel Extension If you want to call `Retriable.retriable` without the `Retriable` module prefix and you don't mind extending `Kernel`, there is a kernel extension available for this. In your ruby script: ```ruby require 'retriable/core_ext/kernel' ``` or in your Gemfile: ```ruby gem 'retriable', require: 'retriable/core_ext/kernel' ``` and then you can call `#retriable` in any context like this: ```ruby retriable do # code here... end ``` ## Proxy Wrapper Object [@julik](https://github.com/julik) has created a gem called [retriable_proxy](https://github.com/julik/retriable_proxy) that extends `retriable` with the ability to wrap objects and specify which methods you want to be retriable, like so: ```ruby # api_endpoint is an instance of some kind of class that connects to an API RetriableProxy.for_object(api_endpoint, on: Net::TimeoutError) ``` ## Credits The randomized exponential backoff implementation was inspired by the one used in Google's [google-http-java-client](https://code.google.com/p/google-http-java-client/wiki/ExponentialBackoff) project. retriable-3.0.1/Guardfile0000644000175000017500000000037113054002021014321 0ustar pravipravi# A sample Guardfile # More info at https://github.com/guard/guard#readme guard :minitest do watch(%r{^spec/(.*)_spec\.rb}) watch(%r{^lib/retriable/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^spec/spec_helper\.rb}) { "spec" } end retriable-3.0.1/retriable.gemspec0000644000175000017500000000270313054002021016013 0ustar pravipravi# coding: utf-8 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "retriable/version" Gem::Specification.new do |spec| spec.name = "retriable" spec.version = Retriable::VERSION spec.authors = ["Jack Chu"] spec.email = ["jack@jackchu.com"] spec.summary = "Retriable is an simple DSL to retry failed code blocks with randomized exponential backoff" spec.description = "Retriable is an simple DSL to retry failed code blocks with randomized exponential backoff. This is especially useful when interacting external api/services or file system calls." spec.homepage = "http://github.com/kamui/retriable" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.0.0" spec.add_development_dependency "bundler" spec.add_development_dependency "rake", "~> 12.0" spec.add_development_dependency "minitest", "~> 5.10" spec.add_development_dependency "guard" spec.add_development_dependency "guard-minitest" if RUBY_VERSION < "2.3" spec.add_development_dependency "ruby_dep", "~> 1.3.1" spec.add_development_dependency "listen", "~> 3.0.8" else spec.add_development_dependency "listen", "~> 3.1" end end retriable-3.0.1/.rubocop.yml0000644000175000017500000000107613054002021014751 0ustar pravipraviStyle/StringLiterals: EnforcedStyle: double_quotes Style/Documentation: Enabled: false Style/TrailingCommaInArguments: EnforcedStyleForMultiline: comma Style/TrailingCommaInLiteral: EnforcedStyleForMultiline: comma Style/InheritException: Enabled: false Style/IndentArray: Enabled: false Style/IndentHash: Enabled: false Metrics/ClassLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/LineLength: Enabled: false Metrics/MethodLength: Enabled: false Metrics/BlockLength: Enabled: false Metrics/AbcSize: Enabled: false retriable-3.0.1/lib/0000755000175000017500000000000013054002021013241 5ustar pravipraviretriable-3.0.1/lib/retriable/0000755000175000017500000000000013054002021015212 5ustar pravipraviretriable-3.0.1/lib/retriable/exponential_backoff.rb0000644000175000017500000000206313054002021021541 0ustar pravipravimodule Retriable class ExponentialBackoff attr_accessor :tries attr_accessor :base_interval attr_accessor :multiplier attr_accessor :max_interval attr_accessor :rand_factor def initialize(opts = {}) @tries = opts[:tries] || Retriable.config.tries @base_interval = opts[:base_interval] || Retriable.config.base_interval @max_interval = opts[:max_interval] || Retriable.config.max_interval @rand_factor = opts[:rand_factor] || Retriable.config.rand_factor @multiplier = opts[:multiplier] || Retriable.config.multiplier end def intervals intervals = Array.new(tries) do |iteration| [base_interval * multiplier**iteration, max_interval].min end return intervals if rand_factor.zero? intervals.map { |i| randomize(i) } end private def randomize(interval) return interval if rand_factor.zero? delta = rand_factor * interval * 1.0 min = interval - delta max = interval + delta rand(min..max) end end end retriable-3.0.1/lib/retriable/core_ext/0000755000175000017500000000000013054002021017022 5ustar pravipraviretriable-3.0.1/lib/retriable/core_ext/kernel.rb0000644000175000017500000000020513054002021020624 0ustar pravipravirequire_relative "../../retriable" module Kernel def retriable(opts = {}, &block) Retriable.retriable(opts, &block) end end retriable-3.0.1/lib/retriable/version.rb0000644000175000017500000000006013054002021017220 0ustar pravipravimodule Retriable VERSION = "3.0.1".freeze end retriable-3.0.1/lib/retriable/config.rb0000644000175000017500000000136613054002021017012 0ustar pravipravimodule Retriable class Config attr_accessor :sleep_disabled attr_accessor :tries attr_accessor :base_interval attr_accessor :max_interval attr_accessor :rand_factor attr_accessor :multiplier attr_accessor :max_elapsed_time attr_accessor :intervals attr_accessor :timeout attr_accessor :on attr_accessor :on_retry def initialize @sleep_disabled = false @tries = 3 @base_interval = 0.5 @max_interval = 60 @rand_factor = 0.5 @multiplier = 1.5 @max_elapsed_time = 900 # 15 min @intervals = nil @timeout = nil @on = [StandardError] @on_retry = nil end end end retriable-3.0.1/lib/retriable.rb0000644000175000017500000000411313054002021015536 0ustar pravipravirequire "timeout" require_relative "retriable/config" require_relative "retriable/exponential_backoff" require_relative "retriable/version" module Retriable module_function def self.configure yield(config) end def config @config ||= Config.new end def retriable(opts = {}) tries = opts[:tries] || config.tries base_interval = opts[:base_interval] || config.base_interval max_interval = opts[:max_interval] || config.max_interval rand_factor = opts[:rand_factor] || config.rand_factor multiplier = opts[:multiplier] || config.multiplier max_elapsed_time = opts[:max_elapsed_time] || config.max_elapsed_time intervals = opts[:intervals] || config.intervals timeout = opts[:timeout] || config.timeout on = opts[:on] || config.on on_retry = opts[:on_retry] || config.on_retry start_time = Time.now elapsed_time = -> { Time.now - start_time } if intervals tries = intervals.size + 1 else intervals = ExponentialBackoff.new( tries: tries - 1, base_interval: base_interval, multiplier: multiplier, max_interval: max_interval, rand_factor: rand_factor, ).intervals end exception_list = on.is_a?(Hash) ? on.keys : on tries.times do |index| try = index + 1 begin return Timeout.timeout(timeout) { return yield(try) } if timeout return yield(try) rescue *[*exception_list] => exception if on.is_a?(Hash) raise unless exception_list.any? do |e| exception.is_a?(e) && ([*on[e]].empty? || [*on[e]].any? { |pattern| exception.message =~ pattern }) end end interval = intervals[index] on_retry.call(exception, try, elapsed_time.call, interval) if on_retry raise if try >= tries || (elapsed_time.call + interval) > max_elapsed_time sleep interval if config.sleep_disabled != true end end end end retriable-3.0.1/.travis.yml0000644000175000017500000000102713054002021014604 0ustar pravipravi# Send builds to container-based infrastructure # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ sudo: false language: ruby rvm: - 2.0.0 - 2.1.10 - 2.2.6 - 2.3.3 - 2.4.0 - rbx - jruby-9.0.5.0 - jruby-9.1.7.0 - ruby-head - jruby-head matrix: allow_failures: - rvm: rbx - rvm: ruby-head - rvm: jruby-head before_install: - gem update --system - gem install bundler addons: code_climate: repo_token: 20a1139ef1830b4f813a10a03d90e8aa179b5226f75e75c5a949b25756ebf558 retriable-3.0.1/.gitignore0000644000175000017500000000016613054002021014466 0ustar pravipravi/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log retriable-3.0.1/LICENSE0000644000175000017500000000207113054002021013500 0ustar pravipraviCopyright (c) 2012-2013 Jack Chu (http://www.jackchu.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.retriable-3.0.1/Rakefile0000644000175000017500000000030713054002021014140 0ustar pravipravi# encoding: utf-8 require "bundler" Bundler::GemHelper.install_tasks require "rake/testtask" task default: :test Rake::TestTask.new do |t| t.pattern = "spec/**/*_spec.rb" t.verbose = true end retriable-3.0.1/Gemfile0000644000175000017500000000051213054002021013764 0ustar pravipravisource "https://rubygems.org" # Specify your gem's dependencies in retriable.gemspec gemspec group :test do # gem "ruby_gntp" gem "codeclimate-test-reporter", require: false gem "minitest-focus" gem "simplecov", require: false end group :development do gem "rubocop" end group :development, :test do gem "pry" end retriable-3.0.1/spec/0000755000175000017500000000000013054002021013425 5ustar pravipraviretriable-3.0.1/spec/retriable_spec.rb0000644000175000017500000002162313054002021016741 0ustar pravipravirequire_relative "spec_helper" class TestError < Exception; end describe Retriable do subject do Retriable end before do srand 0 end describe "with sleep disabled" do before do Retriable.configure do |c| c.sleep_disabled = true end end it "stops at first try if the block does not raise an exception" do tries = 0 subject.retriable do tries += 1 end expect(tries).must_equal 1 end it "raises a LocalJumpError if #retriable is not given a block" do expect do subject.retriable on: StandardError end.must_raise LocalJumpError expect do subject.retriable on: StandardError, timeout: 2 end.must_raise LocalJumpError end it "makes 3 tries when retrying block of code raising StandardError with no arguments" do tries = 0 expect do subject.retriable do tries += 1 raise StandardError.new, "StandardError occurred" end end.must_raise StandardError expect(tries).must_equal 3 end it "makes only 1 try when exception raised is not ancestor of StandardError" do tries = 0 expect do subject.retriable do tries += 1 raise TestError.new, "TestError occurred" end end.must_raise TestError expect(tries).must_equal 1 end it "#retriable with custom exception tries 3 times and re-raises the exception" do tries = 0 expect do subject.retriable on: TestError do tries += 1 raise TestError.new, "TestError occurred" end end.must_raise TestError expect(tries).must_equal 3 end it "#retriable tries 10 times" do tries = 0 expect do subject.retriable(tries: 10) do tries += 1 raise StandardError.new, "StandardError occurred" end end.must_raise StandardError expect(tries).must_equal 10 end it "#retriable will timeout after 1 second" do expect do subject.retriable timeout: 1 do sleep 1.1 end end.must_raise Timeout::Error end it "applies a randomized exponential backoff to each try" do tries = 0 time_table = [] handler = lambda do |exception, _try, _elapsed_time, next_interval| expect(exception.class).must_equal ArgumentError time_table << next_interval end expect do Retriable.retriable( on: [EOFError, ArgumentError], on_retry: handler, tries: 10, ) do tries += 1 raise ArgumentError.new, "ArgumentError occurred" end end.must_raise ArgumentError expect(time_table).must_equal([ 0.5244067512211441, 0.9113920238761231, 1.2406087918999114, 1.7632403621664823, 2.338001204738311, 4.350816718580626, 5.339852157217869, 11.889873261212443, 18.756037881636484, nil, ]) expect(tries).must_equal(10) end describe "retries with an on_#retriable handler, 6 max retries, and a 0.0 rand_factor" do before do tries = 6 @try_count = 0 @time_table = {} handler = lambda do |exception, try, _elapsed_time, next_interval| expect(exception.class).must_equal ArgumentError @time_table[try] = next_interval end Retriable.retriable( on: [EOFError, ArgumentError], on_retry: handler, rand_factor: 0.0, tries: tries, ) do @try_count += 1 raise ArgumentError.new, "ArgumentError occurred" if @try_count < tries end end it "makes 6 tries" do expect(@try_count).must_equal 6 end it "applies a non-randomized exponential backoff to each try" do expect(@time_table).must_equal( 1 => 0.5, 2 => 0.75, 3 => 1.125, 4 => 1.6875, 5 => 2.53125, ) end end it "#retriable has a max interval of 1.5 seconds" do tries = 0 time_table = {} handler = lambda do |_exception, try, _elapsed_time, next_interval| time_table[try] = next_interval end expect do subject.retriable( on: StandardError, on_retry: handler, rand_factor: 0.0, tries: 5, max_interval: 1.5, ) do tries += 1 raise StandardError.new, "StandardError occurred" end end.must_raise StandardError expect(time_table).must_equal( 1 => 0.5, 2 => 0.75, 3 => 1.125, 4 => 1.5, 5 => nil, ) end it "#retriable with custom defined intervals" do intervals = [ 0.5, 0.75, 1.125, 1.5, 1.5, ] time_table = {} handler = lambda do |_exception, try, _elapsed_time, next_interval| time_table[try] = next_interval end try_count = 0 expect do subject.retriable( on_retry: handler, intervals: intervals, ) do try_count += 1 raise StandardError.new, "StandardError occurred" end end.must_raise StandardError expect(time_table).must_equal( 1 => 0.5, 2 => 0.75, 3 => 1.125, 4 => 1.5, 5 => 1.5, 6 => nil, ) expect(try_count).must_equal(6) end it "#retriable with a hash exception where the value is an exception message pattern" do e = expect do subject.retriable on: { TestError => /something went wrong/ } do raise TestError, "something went wrong" end end.must_raise TestError expect(e.message).must_equal "something went wrong" end it "#retriable with a hash exception list matches exception subclasses" do class SecondTestError < TestError; end class DifferentTestError < Exception; end tries = 0 e = expect do subject.retriable on: { DifferentTestError => /should never happen/, TestError => /something went wrong/, DifferentTestError => /also should never happen/, }, tries: 4 do tries += 1 raise SecondTestError, "something went wrong" end end.must_raise SecondTestError expect(e.message).must_equal "something went wrong" expect(tries).must_equal 4 end it "#retriable with a hash exception list does not retry matching exception subclass but not message" do class SecondTestError < TestError; end tries = 0 expect do subject.retriable on: { TestError => /something went wrong/ }, tries: 4 do tries += 1 raise SecondTestError, "not a match" end end.must_raise SecondTestError expect(tries).must_equal 1 end it "#retriable with a hash exception list where the values are exception message patterns" do tries = 0 exceptions = [] handler = lambda do |exception, try, _elapsed_time, _next_interval| exceptions[try] = exception end e = expect do subject.retriable tries: 4, on: { StandardError => nil, TestError => [/foo/, /bar/] }, on_retry: handler do tries += 1 case tries when 1 raise TestError, "foo" when 2 raise TestError, "bar" when 3 raise StandardError else raise TestError, "crash" end end end.must_raise TestError expect(e.message).must_equal "crash" expect(exceptions[1].class).must_equal TestError expect(exceptions[1].message).must_equal "foo" expect(exceptions[2].class).must_equal TestError expect(exceptions[2].message).must_equal "bar" expect(exceptions[3].class).must_equal StandardError end it "#retriable can be called in the global scope" do expect do retriable do puts "should raise NoMethodError" end end.must_raise NoMethodError require_relative "../lib/retriable/core_ext/kernel" tries = 0 expect do retriable do tries += 1 raise StandardError end end.must_raise StandardError expect(tries).must_equal 3 end end it "#retriable runs for a max elapsed time of 2 seconds" do subject.configure do |c| c.sleep_disabled = false end expect(subject.config.sleep_disabled).must_equal false tries = 0 time_table = {} handler = lambda do |_exception, try, elapsed_time, _next_interval| time_table[try] = elapsed_time end expect do subject.retriable( base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0, max_elapsed_time: 2.0, on_retry: handler, ) do tries += 1 raise EOFError end end.must_raise EOFError expect(tries).must_equal 2 end end retriable-3.0.1/spec/config_spec.rb0000644000175000017500000000204413054002021016231 0ustar pravipravirequire_relative "spec_helper" describe Retriable::Config do subject do Retriable::Config end it "sleep defaults to enabled" do expect(subject.new.sleep_disabled).must_equal false end it "tries defaults to 3" do expect(subject.new.tries).must_equal 3 end it "max interval defaults to 60" do expect(subject.new.max_interval).must_equal 60 end it "randomization factor defaults to 0.5" do expect(subject.new.base_interval).must_equal 0.5 end it "multiplier defaults to 1.5" do expect(subject.new.multiplier).must_equal 1.5 end it "max elapsed time defaults to 900" do expect(subject.new.max_elapsed_time).must_equal 900 end it "intervals defaults to nil" do expect(subject.new.intervals).must_be_nil end it "timeout defaults to nil" do expect(subject.new.timeout).must_be_nil end it "on defaults to [StandardError]" do expect(subject.new.on).must_equal [StandardError] end it "on retry handler defaults to nil" do expect(subject.new.on_retry).must_be_nil end end retriable-3.0.1/spec/spec_helper.rb0000644000175000017500000000041313054002021016241 0ustar pravipravirequire "codeclimate-test-reporter" require "simplecov" CodeClimate::TestReporter.configure do |config| config.logger.level = Logger::WARN end SimpleCov.start require "minitest/autorun" require "minitest/focus" require "pry" require_relative "../lib/retriable" retriable-3.0.1/spec/exponential_backoff_spec.rb0000644000175000017500000000414113054002021020765 0ustar pravipravirequire_relative "spec_helper" describe Retriable::ExponentialBackoff do subject do Retriable::ExponentialBackoff end before do srand 0 end it "tries defaults to 3" do expect(subject.new.tries).must_equal 3 end it "max interval defaults to 60" do expect(subject.new.max_interval).must_equal 60 end it "randomization factor defaults to 0.5" do expect(subject.new.base_interval).must_equal 0.5 end it "multiplier defaults to 1.5" do expect(subject.new.multiplier).must_equal 1.5 end it "generates 10 randomized intervals" do expect(subject.new(tries: 9).intervals).must_equal([ 0.5244067512211441, 0.9113920238761231, 1.2406087918999114, 1.7632403621664823, 2.338001204738311, 4.350816718580626, 5.339852157217869, 11.889873261212443, 18.756037881636484, ]) end it "generates defined number of intervals" do expect(subject.new(tries: 5).intervals.size).must_equal 5 end it "generates intervals with a defined base interval" do expect(subject.new(base_interval: 1).intervals).must_equal([ 1.0488135024422882, 1.8227840477522461, 2.4812175837998227, ]) end it "generates intervals with a defined multiplier" do expect(subject.new(multiplier: 1).intervals).must_equal([ 0.5244067512211441, 0.607594682584082, 0.5513816852888495, ]) end it "generates intervals with a defined max interval" do expect(subject.new(max_interval: 1.0, rand_factor: 0.0).intervals).must_equal([ 0.5, 0.75, 1.0, ]) end it "generates intervals with a defined rand_factor" do expect(subject.new(rand_factor: 0.2).intervals).must_equal([ 0.5097627004884576, 0.8145568095504492, 1.1712435167599646, ]) end it "generates 10 non-randomized intervals" do expect(subject.new( tries: 10, rand_factor: 0.0, ).intervals).must_equal([ 0.5, 0.75, 1.125, 1.6875, 2.53125, 3.796875, 5.6953125, 8.54296875, 12.814453125, 19.2216796875, ]) end end