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

Add on_max_elapsed_time callback #77

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## HEAD

* Add `on_max_elapsed_time` callback

## 3.1.2

* Replace `minitest` gem with `rspec`
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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). |
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks). |
| **`on_max_elapsed_time`** | `nil` | `Proc` to call if the max elapsed time has been reached. [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. |
| **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach. |
Expand Down Expand Up @@ -204,12 +205,19 @@ end

`#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.

`#retriable` also provides a callback called `:on_max_elapsed_time` that will run before the exception is raised if the reason for raising the exception is that the max elapsed time will be reached. This callback provides all the same parameters has `:on_retry` except for `next_interval`.

```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
do_this_on_max_elapsed_time_error = Proc.new do |exception, try, elapsed_time|
log "#{exception.class}: '#{exception.message}' - #{try} tries in #{elapsed_time} seconds."
log "Would be over max elapsed time with another retry. No more retries"
end

Retriable.retriable(on_retry: do_this_on_each_retry, on_max_elapsed_time: do_this_on_max_elapsed_time_error) do
# code here...
end
```
Expand Down
10 changes: 9 additions & 1 deletion lib/retriable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def retriable(opts = {})
timeout = local_config.timeout
on = local_config.on
on_retry = local_config.on_retry
on_max_elapsed_time = local_config.on_max_elapsed_time
sleep_disabled = local_config.sleep_disabled

exception_list = on.is_a?(Hash) ? on.keys : on
Expand Down Expand Up @@ -68,7 +69,14 @@ def retriable(opts = {})

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
raise if try >= tries

current_elapsed_time = elapsed_time.call
if (current_elapsed_time + interval) > max_elapsed_time
on_max_elapsed_time.call(exception, try, current_elapsed_time) if on_max_elapsed_time
raise
end

sleep interval if sleep_disabled != true
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 @@ -9,6 +9,7 @@ class Config
:timeout,
:on,
:on_retry,
:on_max_elapsed_time,
:contexts,
].freeze

Expand All @@ -28,6 +29,7 @@ def initialize(opts = {})
@timeout = nil
@on = [StandardError]
@on_retry = nil
@on_max_elapsed_time = nil
@contexts = {}

opts.each do |k, v|
Expand Down
4 changes: 4 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
expect(default_config.on_retry).to be_nil
end

it "on_max_elapsed_time handler defaults to nil" do
expect(default_config.on_max_elapsed_time).to be_nil
end

it "contexts defaults to {}" do
expect(default_config.contexts).to eq({})
end
Expand Down
38 changes: 38 additions & 0 deletions spec/retriable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,44 @@ def increment_tries_with_exception(exception_class = nil)
expect(@tries).to eq(2)
end

it "calls the on_max_elapsed_time proc" do
described_class.configure { |c| c.sleep_disabled = false }

exceptions = []
actual_elapsed_time = nil
handler = Proc.new do |exception, try, elapsed_time|
exceptions[try] = exception
actual_elapsed_time = elapsed_time
end

expect do
described_class.retriable(base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0,
max_elapsed_time: 2.0, on_max_elapsed_time: handler) do
increment_tries_with_exception
end
end.to raise_error(StandardError)

expect(exceptions[1]).to be_nil
expect(exceptions[2]).to be_a(StandardError)
expect(actual_elapsed_time).not_to be_nil
expect(actual_elapsed_time).to be > 1
end

it "does not call the on_max_elapsed_time proc if max elapsed time is not reached" do
described_class.configure { |c| c.sleep_disabled = false }

handler = Proc.new do |_exception, _try, _elapsed_time|
raise NonStandardError, "this callback should not have been called"
end

expect do
described_class.retriable(base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0,
max_elapsed_time: 10.0, on_max_elapsed_time: handler) do
increment_tries_with_exception
end
end.to raise_error(StandardError)
end

it "raises ArgumentError on invalid options" do
expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError)
end
Expand Down