Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow unlimited tries #95

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
Expand Down
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: 2.0

Style/StringLiterals:
EnforcedStyle: double_quotes

Expand Down
21 changes: 21 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2020-12-28 23:59:17 -0500 using RuboCop version 0.50.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 1
Metrics/CyclomaticComplexity:
Max: 14

# Offense count: 2
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 202

# Offense count: 1
Metrics/PerceivedComplexity:
Max: 15
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

source "https://rubygems.org"

gemspec
Expand All @@ -8,7 +10,8 @@ group :test do
end

group :development do
gem "rubocop"
gem "rubocop", ">= 0.50", "< 0.51", require: false
gem "rubocop-rspec", require: false
end

group :development, :test do
Expand Down
68 changes: 68 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
PATH
remote: .
specs:
retriable (3.1.2)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
coderay (1.1.3)
diff-lcs (1.4.4)
docile (1.3.5)
json (2.5.1)
method_source (1.0.0)
parallel (1.13.0)
parser (2.7.2.0)
ast (~> 2.4.1)
powerpack (0.1.3)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
rainbow (2.2.2)
rake
rake (12.3.3)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.1)
rubocop (0.50.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.5.1)
rubocop (>= 0.41.2)
ruby-progressbar (1.11.0)
simplecov (0.17.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
unicode-display_width (1.7.0)

PLATFORMS
ruby

DEPENDENCIES
bundler
pry
retriable!
rspec
rubocop (>= 0.50, < 0.51)
rubocop-rspec
simplecov

BUNDLED WITH
1.17.3
45 changes: 32 additions & 13 deletions lib/retriable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,35 +42,54 @@ def retriable(opts = {})
elapsed_time = -> { Time.now - start_time }

if intervals
tries = intervals.size + 1
tries = intervals.size + 1 if intervals.size
else
intervals = ExponentialBackoff.new(
tries: tries - 1,
backoff = ExponentialBackoff.new(
tries: tries ? tries - 1 : nil,
base_interval: base_interval,
multiplier: multiplier,
max_interval: max_interval,
rand_factor: rand_factor
).intervals
rand_factor: rand_factor,
)
intervals = backoff.intervals
end

tries.times do |index|
try = index + 1

# TODO: Ideally this would be it's own function, but would probably require
# a separate class to efficiently pass on the processed config
run_try = Proc.new do |interval, try|
begin
return Timeout.timeout(timeout) { return yield(try) } if timeout
return yield(try)
rescue *[*exception_list] => exception
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 sleep_disabled != true


# Note: Tries can't always be calculated if a custom Enumerator is given
# for the intervals argument. (Enumerator#size will return nil, per docs)
# So we'll just let the enumerator run out, and then invoke run_try once more.
# With a nil timeout.
raise unless interval
raise if max_elapsed_time && elapsed_time.call + interval > max_elapsed_time
sleep interval unless sleep_disabled
throw :failed
end
end

try = 0
intervals.each do |interval|
try += 1
# Use throw/catch to distinguish between success and caught exception
catch :failed do
result = run_try.call(interval, try)
return result
end
break if tries and try + 1 >= tries
end
run_try.call(nil, try + 1)
end
end
18 changes: 9 additions & 9 deletions lib/retriable/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

module Retriable
class Config
ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + [
:sleep_disabled,
:max_elapsed_time,
:intervals,
:timeout,
:on,
:on_retry,
:contexts,
]).freeze
ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + %i[
sleep_disabled
max_elapsed_time
intervals
timeout
on
on_retry
contexts
]).freeze

attr_accessor(*ATTRIBUTES)

Expand Down
32 changes: 18 additions & 14 deletions lib/retriable/exponential_backoff.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module Retriable
class ExponentialBackoff
ATTRIBUTES = [
:tries,
:base_interval,
:multiplier,
:max_interval,
:rand_factor,
].freeze
ATTRIBUTES = %i[
tries
base_interval
multiplier
max_interval
rand_factor
].freeze

attr_accessor(*ATTRIBUTES)

Expand All @@ -24,18 +24,22 @@ def initialize(opts = {})
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) }
Enumerator.new(tries) do |result|
try = 0
loop do
interval = [base_interval * multiplier**try, max_interval].min
result << randomize(interval)
try += 1
raise StopIteration if tries && try >= tries
end
end.lazy
end

private

def randomize(interval)
return interval if rand_factor.zero?

delta = rand_factor * interval * 1.0
min = interval - delta
max = interval + delta
Expand Down
8 changes: 1 addition & 7 deletions retriable.gemspec
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# coding: utf-8

lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "retriable/version"
Expand All @@ -23,10 +23,4 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler"
spec.add_development_dependency "rspec", "~> 3"

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
20 changes: 10 additions & 10 deletions spec/exponential_backoff_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
end

it "generates 10 randomized intervals" do
expect(described_class.new(tries: 9).intervals).to eq([
expect(described_class.new(tries: 9).intervals.to_a).to eq([
0.5244067512211441,
0.9113920238761231,
1.2406087918999114,
Expand All @@ -29,7 +29,7 @@
4.350816718580626,
5.339852157217869,
11.889873261212443,
18.756037881636484,
18.756037881636484
])
end

Expand All @@ -38,35 +38,35 @@
end

it "generates intervals with a defined base interval" do
expect(described_class.new(base_interval: 1).intervals).to eq([
expect(described_class.new(base_interval: 1).intervals.to_a).to eq([
1.0488135024422882,
1.8227840477522461,
2.4812175837998227,
2.4812175837998227
])
end

it "generates intervals with a defined multiplier" do
expect(described_class.new(multiplier: 1).intervals).to eq([
expect(described_class.new(multiplier: 1).intervals.to_a).to eq([
0.5244067512211441,
0.607594682584082,
0.5513816852888495,
0.5513816852888495
])
end

it "generates intervals with a defined max interval" do
expect(described_class.new(max_interval: 1.0, rand_factor: 0.0).intervals).to eq([0.5, 0.75, 1.0])
expect(described_class.new(max_interval: 1.0, rand_factor: 0.0).intervals.to_a).to eq([0.5, 0.75, 1.0])
end

it "generates intervals with a defined rand_factor" do
expect(described_class.new(rand_factor: 0.2).intervals).to eq([
expect(described_class.new(rand_factor: 0.2).intervals.to_a).to eq([
0.5097627004884576,
0.8145568095504492,
1.1712435167599646,
1.1712435167599646
])
end

it "generates 10 non-randomized intervals" do
non_random_intervals = 9.times.inject([0.5]) { |memo, _i| memo + [memo.last * 1.5] }
expect(described_class.new(tries: 10, rand_factor: 0.0).intervals).to eq(non_random_intervals)
expect(described_class.new(tries: 10, rand_factor: 0.0).intervals.to_a).to eq(non_random_intervals)
end
end
Loading