Skip to content

Commit

Permalink
Allow not to retry some exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Aug 25, 2019
1 parent b911087 commit c897852
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 53 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Here are the available options, in some vague order of relevance to most common
| ------ | ------- | ---------- |
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). |
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
| **`not`** | `[]` | Type of exceptions not to retry. Takes precedence over `on`. [Read more](#configuring-which-options-not-to-retry-with-not)
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks). |
| **`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. |
Expand All @@ -100,6 +101,17 @@ Here are the available options, in some vague order of relevance to most common
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
- An array of patterns (retries exceptions ONLY if their `message` matches at least one of the patterns)

#### Configuring Which Options Not to Retry With :not
**`:not`** Can take the same form as `:on`.

Example usage:
```ruby
class MyError < StandardError; end

Retriable.retriable(on: StandardError, not: MyError) do
raise MyError, "No retries!"
end
```

### Configuration

Expand Down
116 changes: 63 additions & 53 deletions lib/retriable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,82 @@
require_relative "retriable/version"

module Retriable
module_function
class << self
def configure
yield(config)
end

def configure
yield(config)
end
def config
@config ||= Config.new
end

def config
@config ||= Config.new
end
def with_context(context_key, options = {}, &block)
if !config.contexts.key?(context_key)
raise ArgumentError, "#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
end

def with_context(context_key, options = {}, &block)
if !config.contexts.key?(context_key)
raise ArgumentError, "#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
retriable(config.contexts[context_key].merge(options), &block) if block
end

retriable(config.contexts[context_key].merge(options), &block) if block
end
def retriable(opts = {})
local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))

def retriable(opts = {})
local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))
tries = local_config.tries
base_interval = local_config.base_interval
max_interval = local_config.max_interval
rand_factor = local_config.rand_factor
multiplier = local_config.multiplier
max_elapsed_time = local_config.max_elapsed_time
intervals = local_config.intervals
timeout = local_config.timeout
on = local_config.on
not_ = local_config.not
on_retry = local_config.on_retry
sleep_disabled = local_config.sleep_disabled

tries = local_config.tries
base_interval = local_config.base_interval
max_interval = local_config.max_interval
rand_factor = local_config.rand_factor
multiplier = local_config.multiplier
max_elapsed_time = local_config.max_elapsed_time
intervals = local_config.intervals
timeout = local_config.timeout
on = local_config.on
on_retry = local_config.on_retry
sleep_disabled = local_config.sleep_disabled
on_exceptions = on.is_a?(Hash) ? on.keys : on
not_exceptions = not_.is_a?(Hash) ? not_.keys : not_
start_time = Time.now
elapsed_time = -> { Time.now - start_time }

exception_list = on.is_a?(Hash) ? on.keys : on
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

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
tries.times do |index|
try = index + 1

tries.times do |index|
try = index + 1
begin
return Timeout.timeout(timeout) { return yield(try) } if timeout
return yield(try)
rescue *not_exceptions => exception
raise if !not_.is_a?(Hash) || matches?(exception, not_)
rescue *on_exceptions => exception
raise if on.is_a?(Hash) && !matches?(exception, on)

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
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
end
end
end

private

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
def matches?(exception, exceptions)
exception_list = exceptions.is_a?(Hash) ? exceptions.keys : exceptions
exception_list.any? do |e|
exception.is_a?(e) &&
([*exceptions[e]].empty? || [*exceptions[e]].any? { |pattern| exception.message =~ pattern })
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/retriable/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Config
:intervals,
:timeout,
:on,
:not,
:on_retry,
:contexts,
]).freeze
Expand All @@ -27,6 +28,7 @@ def initialize(opts = {})
@intervals = nil
@timeout = nil
@on = [StandardError]
@not = []
@on_retry = nil
@contexts = {}

Expand Down
20 changes: 20 additions & 0 deletions spec/retriable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,26 @@ def increment_tries_with_exception(exception_class = nil)
end
end

context "with :not parameter" do
it "does not retry on :not exception" do
expect do
described_class.retriable(not: StandardError) { increment_tries_with_exception }
end.to raise_error(StandardError)

expect(@tries).to eq(1)
end

it "gives precedence to :not over :on" do
expect do
described_class.retriable(on: StandardError, not: IndexError) do
increment_tries_with_exception(@tries >= 1 ? IndexError : StandardError)
end
end.to raise_error(IndexError)

expect(@tries).to eq(2)
end
end

it "runs for a max elapsed time of 2 seconds" do
described_class.configure { |c| c.sleep_disabled = false }

Expand Down

0 comments on commit c897852

Please sign in to comment.