diff --git a/CHANGELOG.md b/CHANGELOG.md index 7804197..3f37216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Replace `minitest` gem with `rspec` * Fancier README * Remove unnecessary short circuit in `randomize` method +* Add ability for `:on` argument to accept a `Hash` where the keys are exception types and the values are either `Proc`s or arrays of `Proc`s that will evaluate to `true` when the exception should be retried. ## 3.1.1 diff --git a/README.md b/README.md index 551b14c..880498b 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,9 @@ Here are the available options, in some vague order of relevance to most common - An `Array` of `Exception` classes (retry any exception of one of these types, including subclasses) - A `Hash` where the keys are `Exception` classes and the values are one of: - `nil` (retry every exception of the key's type, including subclasses) + - A single `Proc` (retries exceptions ONLY if it returns truthy) - 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) + - An array of `Proc` and/or `Regexp` (retries exceptions ONLY if at least one exception message matches a `Regexp` or at least one `Proc` returns `truthy`) ### Configuration @@ -132,10 +133,15 @@ end You can also use a hash to specify that you only want to retry exceptions with certain messages (see [the documentation above](#configuring-which-options-to-retry-with-on)). This example will retry all `ActiveRecord::RecordNotUnique` exceptions, `ActiveRecord::RecordInvalid` exceptions where the message matches either `/Parent must exist/` or `/Username has already been taken/`, or `Mysql2::Error` exceptions where the message matches `/Duplicate entry/`. +A `Regexp` (or array of `Regexp`s). If any of the `Regexp`s match the exception's message, the block will be retried. +A `Proc` (or array of `Proc`s) that evaluates the exception being handled and returns `true` if the block should be retried. If any of the procs in the list return `true`, the block will be retried. +You can also mix and match `Proc`s and `Regexp`s in an `Array` + ```ruby Retriable.retriable(on: { ActiveRecord::RecordNotUnique => nil, ActiveRecord::RecordInvalid => [/Parent must exist/, /Username has already been taken/], + ActiveRecord::RecordNotFound => -> (exception, try, elapsed_time, next_interval) { exception.model == User } Mysql2::Error => /Duplicate entry/ }) do # code here... diff --git a/lib/retriable.rb b/lib/retriable.rb index c944093..a80adc1 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -60,17 +60,33 @@ def retriable(opts = {}) 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] + raise unless matched_exception?(on, exception, try, elapsed_time.call, interval) + 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 + + def matched_exception?(on, exception, *proc_args) + return true unless on.is_a?(Hash) + + on.any? do |expected_exception, matchers| + next false unless exception.is_a?(expected_exception) + next true if matchers.nil? + + Array(matchers).any? do |matcher| + if matcher.is_a?(Regexp) + exception.message =~ matcher + elsif matcher.is_a?(Proc) + matcher.call(exception, *proc_args) + else + raise ArgumentError, "Exception hash values must be Proc or Regexp" + end + end + end + end + private_class_method :matched_exception? end diff --git a/spec/retriable_spec.rb b/spec/retriable_spec.rb index 5084e2f..f438eff 100644 --- a/spec/retriable_spec.rb +++ b/spec/retriable_spec.rb @@ -207,6 +207,34 @@ def increment_tries_with_exception(exception_class = nil) end end + it "#retriable retries with a hash exception where the value is a proc that returns true" do + matcher = lambda do |e, _try, _elapsed_time, _next_interval| + e.message == "something went wrong" + end + tries = 0 + expect { + subject.retriable on: { NonStandardError => matcher }, tries: 2 do + tries += 1 + raise NonStandardError, "something went wrong" + end + }.to raise_error NonStandardError + + expect(tries).to eq 2 + end + + it "#retriable does not retry with a hash exception where the value is a proc that returns false" do + matcher = ->(e, *_args) { e.message == "something went wrong" } + tries = 0 + expect { + subject.retriable on: { NonStandardError => matcher }, tries: 2 do + tries += 1 + raise NonStandardError, "not a match" + end + }.to raise_error NonStandardError + + expect(tries).to eq 1 + end + it "runs for a max elapsed time of 2 seconds" do described_class.configure { |c| c.sleep_disabled = false }