Skip to content

Commit

Permalink
Add a simple context plugin (#43)
Browse files Browse the repository at this point in the history
* Add a simple context feature

* Move context feature into core

* Pluralize config.context to config.contexts

* Rename retriable_with_context to with_context, unless it's in the kernel extension to keep it descriptive

* Remove unnecessary trailing comma in arg list

* Add available contexts in the exception message when calling a context key that doesn't exist
  • Loading branch information
kamui authored Jul 28, 2017
1 parent 69cbcfe commit d9976a8
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 3 deletions.
5 changes: 4 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Style/Documentation:

Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: comma

Style/TrailingCommaInLiteral:
EnforcedStyleForMultiline: comma

Expand All @@ -19,6 +19,9 @@ Style/IndentArray:
Style/IndentHash:
Enabled: false

Style/NegatedIf:
Enabled: false

Metrics/ClassLength:
Enabled: false

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## HEAD

* Added contexts feature. Thanks to @apurvis.

## 3.0.2

* Add configuration and options validation.
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,49 @@ ensure
end
```

## Contexts

Contexts allow you to coordinate sets of Retriable options across an application. Each context is basically an argument hash for `Retriable.retriable` that is stored in the `Retriable.config` as a simple `Hash` and is accessible by name. For example:

```ruby
Retriable.configure do |c|
c.contexts[:aws] = {
tries: 3,
base_interval: 5,
on_retry: Proc.new { puts 'Curse you, AWS!' }
}
c.contexts[:mysql] = {
tries: 10,
multiplier: 2.5,
on: Mysql::DeadlockException
}
end
```

This will create two contexts, `aws` and `mysql`, which allow you to reuse different backoff strategies across your application without continually passing those strategy options to the `retriable` method.

These are used simply by calling `Retriable.with_context`:

```ruby
# Will retry all exceptions
Retriable.with_context(:aws) do
# aws_call
end

# Will retry Mysql::DeadlockException
Retriable.with_context(:mysql) do
# write_to_table
end
```

You can even temporarily override individual options for a configured context:

```ruby
Retriable.with_context(:mysql, tries: 30) do
# write_to_table
end
```

## Kernel Extension

If you want to call `Retriable.retriable` without the `Retriable` module prefix and you don't mind extending `Kernel`,
Expand All @@ -246,6 +289,10 @@ and then you can call `#retriable` in any context like this:
retriable do
# code here...
end

retriable_with_context(:api) do
# code here...
end
```

## Proxy Wrapper Object
Expand Down
12 changes: 10 additions & 2 deletions lib/retriable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
module Retriable
module_function

def self.configure
def configure
yield(config)
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. Here the available contexts: #{config.contexts.keys}"
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))

Expand All @@ -40,7 +48,7 @@ def retriable(opts = {})
base_interval: base_interval,
multiplier: multiplier,
max_interval: max_interval,
rand_factor: rand_factor,
rand_factor: rand_factor
).intervals
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,
:contexts,
].freeze

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

opts.each do |k, v|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
Expand Down
4 changes: 4 additions & 0 deletions lib/retriable/core_ext/kernel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ module Kernel
def retriable(opts = {}, &block)
Retriable.retriable(opts, &block)
end

def retriable_with_context(context_key, opts = {}, &block)
Retriable.with_context(context_key, opts, &block)
end
end
4 changes: 4 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
expect(subject.new.on_retry).must_be_nil
end

it "contexts defaults to {}" do
expect(subject.new.contexts).must_equal Hash.new
end

it "raises errors on invalid configuration" do
assert_raises ArgumentError do
subject.new(does_not_exist: 123)
Expand Down
55 changes: 55 additions & 0 deletions spec/retriable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,59 @@ class SecondTestError < TestError; end
Retriable.retriable(does_not_exist: 123)
end
end

describe "#with_context" do
before do
Retriable.configure do |c|
c.sleep_disabled = true
c.contexts[:sql] = { tries: 1 }
c.contexts[:api] = { tries: 3 }
end
end

it "sql context stops at first try if the block does not raise an exception" do
tries = 0
subject.with_context(:sql) do
tries += 1
end

expect(tries).must_equal 1
end

it "with_context respects the context options" do
tries = 0

expect do
subject.with_context(:api) do
tries += 1
raise StandardError.new, "StandardError occurred"
end
end.must_raise StandardError

expect(tries).must_equal 3
end

it "with_context allows override options" do
tries = 0

expect do
subject.with_context(:sql, tries: 5) do
tries += 1
raise StandardError.new, "StandardError occurred"
end
end.must_raise StandardError

expect(tries).must_equal 5
end

it "raises an ArgumentError when the context isn't found" do
tries = 0

expect do
subject.with_context(:wtf) do
tries += 1
end
end.must_raise ArgumentError
end
end
end

0 comments on commit d9976a8

Please sign in to comment.