From 94b1defab058c7f58087905d6fb8d025cb2a718d Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Wed, 26 Jul 2017 23:08:00 -0400 Subject: [PATCH 1/6] Add a simple context plugin --- .rubocop.yml | 5 ++- CHANGELOG.md | 2 ++ README.md | 49 ++++++++++++++++++++++++++++ lib/retriable.rb | 2 +- lib/retriable/context.rb | 16 ++++++++++ spec/context_spec.rb | 69 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 lib/retriable/context.rb create mode 100644 spec/context_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index e2858af..06c59a2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,7 +6,7 @@ Style/Documentation: Style/TrailingCommaInArguments: EnforcedStyleForMultiline: comma - + Style/TrailingCommaInLiteral: EnforcedStyleForMultiline: comma @@ -19,6 +19,9 @@ Style/IndentArray: Style/IndentHash: Enabled: false +Style/NegatedIf: + Enabled: false + Metrics/ClassLength: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 169f779..fee6b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## HEAD +* Added contexts feature. Thanks to @apurvis. + ## 3.0.2 * Add configuration and options validation. diff --git a/README.md b/README.md index 02256e0..0cc5018 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,55 @@ ensure end ``` +## Contexts + +There is a separate plugin to enable the context feature. It's not included by default; to use it do: + +```ruby +require 'retriable/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` module as a simple `Hash` and is accessible by name. For example: + +```ruby +Retriable.configure do |config, context| + context[:aws] = { + tries: 3, + base_interval: 5, + on_retry: Proc.new { puts 'Curse you, AWS!' } + } + 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`, diff --git a/lib/retriable.rb b/lib/retriable.rb index e58bbeb..f465d46 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -6,7 +6,7 @@ module Retriable module_function - def self.configure + def configure yield(config) end diff --git a/lib/retriable/context.rb b/lib/retriable/context.rb new file mode 100644 index 0000000..fa5aa49 --- /dev/null +++ b/lib/retriable/context.rb @@ -0,0 +1,16 @@ +module Retriable + module_function + + def configure + yield(config, context) + end + + def context + @context ||= {} + end + + def with_context(key, options = {}, &block) + raise ArgumentError, "Context #{key} is not found." if !context.key?(key) + retriable(context[key].merge(options), &block) if block + end +end diff --git a/spec/context_spec.rb b/spec/context_spec.rb new file mode 100644 index 0000000..03b3443 --- /dev/null +++ b/spec/context_spec.rb @@ -0,0 +1,69 @@ +require_relative "spec_helper" + +class TestError < Exception; end + +describe "Retriable Context" do + subject do + Retriable + end + + before do + require_relative "../lib/retriable/context" + srand 0 + end + + describe "with context and sleep disabled" do + before do + Retriable.configure do |c, context| + c.sleep_disabled = true + context[:sql] = { tries: 1 } + context[: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 From 4e35ddc5cf1235637d4321801b44565d9751e378 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Thu, 27 Jul 2017 02:53:28 -0400 Subject: [PATCH 2/6] Move context feature into core --- README.md | 26 ++++++------ lib/retriable.rb | 8 ++++ lib/retriable/config.rb | 2 + lib/retriable/context.rb | 16 -------- lib/retriable/core_ext/kernel.rb | 4 ++ spec/config_spec.rb | 4 ++ spec/context_spec.rb | 69 -------------------------------- spec/retriable_spec.rb | 55 +++++++++++++++++++++++++ 8 files changed, 85 insertions(+), 99 deletions(-) delete mode 100644 lib/retriable/context.rb delete mode 100644 spec/context_spec.rb diff --git a/README.md b/README.md index 0cc5018..02b6d14 100644 --- a/README.md +++ b/README.md @@ -225,22 +225,16 @@ end ## Contexts -There is a separate plugin to enable the context feature. It's not included by default; to use it do: +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 -require 'retriable/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` module as a simple `Hash` and is accessible by name. For example: - -```ruby -Retriable.configure do |config, context| - context[:aws] = { +Retriable.configure do |c| + c.context[:aws] = { tries: 3, base_interval: 5, on_retry: Proc.new { puts 'Curse you, AWS!' } } - contexts[:mysql] = { + c.context[:mysql] = { tries: 10, multiplier: 2.5, on: Mysql::DeadlockException @@ -250,16 +244,16 @@ 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`: +These are used simply by calling `Retriable.retriable_with_context`: ```ruby # Will retry all exceptions -Retriable.with_context(:aws) do +Retriable.retriable_with_context(:aws) do # aws_call end # Will retry Mysql::DeadlockException -Retriable.with_context(:mysql) do +Retriable.retriable_with_context(:mysql) do # write_to_table end ``` @@ -267,7 +261,7 @@ end You can even temporarily override individual options for a configured context: ```ruby -Retriable.with_context(:mysql, tries: 30) do +Retriable.retriable_with_context(:mysql, tries: 30) do # write_to_table end ``` @@ -295,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 diff --git a/lib/retriable.rb b/lib/retriable.rb index f465d46..b37f544 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -14,6 +14,14 @@ def config @config ||= Config.new end + def retriable_with_context(context_key, options = {}, &block) + if !config.context.key?(context_key) + raise ArgumentError, "#{context_key} not found in Retriable.config.context" + end + + retriable(config.context[context_key].merge(options), &block) if block + end + def retriable(opts = {}) local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts)) diff --git a/lib/retriable/config.rb b/lib/retriable/config.rb index 531c060..f186b70 100644 --- a/lib/retriable/config.rb +++ b/lib/retriable/config.rb @@ -9,6 +9,7 @@ class Config :timeout, :on, :on_retry, + :context, ].freeze attr_accessor(*ATTRIBUTES) @@ -27,6 +28,7 @@ def initialize(opts = {}) @timeout = nil @on = [StandardError] @on_retry = nil + @context = {} opts.each do |k, v| raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k) diff --git a/lib/retriable/context.rb b/lib/retriable/context.rb deleted file mode 100644 index fa5aa49..0000000 --- a/lib/retriable/context.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Retriable - module_function - - def configure - yield(config, context) - end - - def context - @context ||= {} - end - - def with_context(key, options = {}, &block) - raise ArgumentError, "Context #{key} is not found." if !context.key?(key) - retriable(context[key].merge(options), &block) if block - end -end diff --git a/lib/retriable/core_ext/kernel.rb b/lib/retriable/core_ext/kernel.rb index d43073a..18291ba 100644 --- a/lib/retriable/core_ext/kernel.rb +++ b/lib/retriable/core_ext/kernel.rb @@ -4,4 +4,8 @@ module Kernel def retriable(opts = {}, &block) Retriable.retriable(opts, &block) end + + def retriable_with_context(context_key, opts = {}, &block) + Retriable.retriable_with_context(context_key, opts, &block) + end end diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 181a0fd..9a9b2e1 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -45,6 +45,10 @@ expect(subject.new.on_retry).must_be_nil end + it "context defaults to {}" do + expect(subject.new.context).must_equal Hash.new + end + it "raises errors on invalid configuration" do assert_raises ArgumentError do subject.new(does_not_exist: 123) diff --git a/spec/context_spec.rb b/spec/context_spec.rb deleted file mode 100644 index 03b3443..0000000 --- a/spec/context_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require_relative "spec_helper" - -class TestError < Exception; end - -describe "Retriable Context" do - subject do - Retriable - end - - before do - require_relative "../lib/retriable/context" - srand 0 - end - - describe "with context and sleep disabled" do - before do - Retriable.configure do |c, context| - c.sleep_disabled = true - context[:sql] = { tries: 1 } - context[: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 diff --git a/spec/retriable_spec.rb b/spec/retriable_spec.rb index 5abc04a..261fab4 100644 --- a/spec/retriable_spec.rb +++ b/spec/retriable_spec.rb @@ -376,4 +376,59 @@ class SecondTestError < TestError; end Retriable.retriable(does_not_exist: 123) end end + + describe "#retriable_with_context" do + before do + Retriable.configure do |c| + c.sleep_disabled = true + c.context[:sql] = { tries: 1 } + c.context[:api] = { tries: 3 } + end + end + + it "sql context stops at first try if the block does not raise an exception" do + tries = 0 + subject.retriable_with_context(:sql) do + tries += 1 + end + + expect(tries).must_equal 1 + end + + it "retriable_with_context respects the context options" do + tries = 0 + + expect do + subject.retriable_with_context(:api) do + tries += 1 + raise StandardError.new, "StandardError occurred" + end + end.must_raise StandardError + + expect(tries).must_equal 3 + end + + it "retriable_with_context allows override options" do + tries = 0 + + expect do + subject.retriable_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.retriable_with_context(:wtf) do + tries += 1 + end + end.must_raise ArgumentError + end + end end From 43da6cd6ac178adcc18e00851330a2cc98121401 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Fri, 28 Jul 2017 01:02:47 -0400 Subject: [PATCH 3/6] Pluralize config.context to config.contexts --- README.md | 4 ++-- lib/retriable.rb | 6 +++--- lib/retriable/config.rb | 4 ++-- spec/config_spec.rb | 4 ++-- spec/retriable_spec.rb | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 02b6d14..31d1534 100644 --- a/README.md +++ b/README.md @@ -229,12 +229,12 @@ Contexts allow you to coordinate sets of Retriable options across an application ```ruby Retriable.configure do |c| - c.context[:aws] = { + c.contexts[:aws] = { tries: 3, base_interval: 5, on_retry: Proc.new { puts 'Curse you, AWS!' } } - c.context[:mysql] = { + c.contexts[:mysql] = { tries: 10, multiplier: 2.5, on: Mysql::DeadlockException diff --git a/lib/retriable.rb b/lib/retriable.rb index b37f544..c10b751 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -15,11 +15,11 @@ def config end def retriable_with_context(context_key, options = {}, &block) - if !config.context.key?(context_key) - raise ArgumentError, "#{context_key} not found in Retriable.config.context" + if !config.contexts.key?(context_key) + raise ArgumentError, "#{context_key} not found in Retriable.config.contexts" end - retriable(config.context[context_key].merge(options), &block) if block + retriable(config.contexts[context_key].merge(options), &block) if block end def retriable(opts = {}) diff --git a/lib/retriable/config.rb b/lib/retriable/config.rb index f186b70..624b9a2 100644 --- a/lib/retriable/config.rb +++ b/lib/retriable/config.rb @@ -9,7 +9,7 @@ class Config :timeout, :on, :on_retry, - :context, + :contexts, ].freeze attr_accessor(*ATTRIBUTES) @@ -28,7 +28,7 @@ def initialize(opts = {}) @timeout = nil @on = [StandardError] @on_retry = nil - @context = {} + @contexts = {} opts.each do |k, v| raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k) diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 9a9b2e1..ddc499d 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -45,8 +45,8 @@ expect(subject.new.on_retry).must_be_nil end - it "context defaults to {}" do - expect(subject.new.context).must_equal Hash.new + it "contexts defaults to {}" do + expect(subject.new.contexts).must_equal Hash.new end it "raises errors on invalid configuration" do diff --git a/spec/retriable_spec.rb b/spec/retriable_spec.rb index 261fab4..ac27ea8 100644 --- a/spec/retriable_spec.rb +++ b/spec/retriable_spec.rb @@ -381,8 +381,8 @@ class SecondTestError < TestError; end before do Retriable.configure do |c| c.sleep_disabled = true - c.context[:sql] = { tries: 1 } - c.context[:api] = { tries: 3 } + c.contexts[:sql] = { tries: 1 } + c.contexts[:api] = { tries: 3 } end end From ea5b10f7e8e0b9be8e4f9fa9130b48e3aa7a4d48 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Fri, 28 Jul 2017 01:06:23 -0400 Subject: [PATCH 4/6] Rename retriable_with_context to with_context, unless it's in the kernel extension to keep it descriptive --- README.md | 8 ++++---- lib/retriable.rb | 2 +- lib/retriable/core_ext/kernel.rb | 2 +- spec/retriable_spec.rb | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 31d1534..6ac8b29 100644 --- a/README.md +++ b/README.md @@ -244,16 +244,16 @@ 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.retriable_with_context`: +These are used simply by calling `Retriable.with_context`: ```ruby # Will retry all exceptions -Retriable.retriable_with_context(:aws) do +Retriable.with_context(:aws) do # aws_call end # Will retry Mysql::DeadlockException -Retriable.retriable_with_context(:mysql) do +Retriable.with_context(:mysql) do # write_to_table end ``` @@ -261,7 +261,7 @@ end You can even temporarily override individual options for a configured context: ```ruby -Retriable.retriable_with_context(:mysql, tries: 30) do +Retriable.with_context(:mysql, tries: 30) do # write_to_table end ``` diff --git a/lib/retriable.rb b/lib/retriable.rb index c10b751..48ed279 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -14,7 +14,7 @@ def config @config ||= Config.new end - def retriable_with_context(context_key, options = {}, &block) + def with_context(context_key, options = {}, &block) if !config.contexts.key?(context_key) raise ArgumentError, "#{context_key} not found in Retriable.config.contexts" end diff --git a/lib/retriable/core_ext/kernel.rb b/lib/retriable/core_ext/kernel.rb index 18291ba..a95c9f2 100644 --- a/lib/retriable/core_ext/kernel.rb +++ b/lib/retriable/core_ext/kernel.rb @@ -6,6 +6,6 @@ def retriable(opts = {}, &block) end def retriable_with_context(context_key, opts = {}, &block) - Retriable.retriable_with_context(context_key, opts, &block) + Retriable.with_context(context_key, opts, &block) end end diff --git a/spec/retriable_spec.rb b/spec/retriable_spec.rb index ac27ea8..49bd2b8 100644 --- a/spec/retriable_spec.rb +++ b/spec/retriable_spec.rb @@ -377,7 +377,7 @@ class SecondTestError < TestError; end end end - describe "#retriable_with_context" do + describe "#with_context" do before do Retriable.configure do |c| c.sleep_disabled = true @@ -388,18 +388,18 @@ class SecondTestError < TestError; end it "sql context stops at first try if the block does not raise an exception" do tries = 0 - subject.retriable_with_context(:sql) do + subject.with_context(:sql) do tries += 1 end expect(tries).must_equal 1 end - it "retriable_with_context respects the context options" do + it "with_context respects the context options" do tries = 0 expect do - subject.retriable_with_context(:api) do + subject.with_context(:api) do tries += 1 raise StandardError.new, "StandardError occurred" end @@ -408,11 +408,11 @@ class SecondTestError < TestError; end expect(tries).must_equal 3 end - it "retriable_with_context allows override options" do + it "with_context allows override options" do tries = 0 expect do - subject.retriable_with_context(:sql, tries: 5) do + subject.with_context(:sql, tries: 5) do tries += 1 raise StandardError.new, "StandardError occurred" end @@ -425,7 +425,7 @@ class SecondTestError < TestError; end tries = 0 expect do - subject.retriable_with_context(:wtf) do + subject.with_context(:wtf) do tries += 1 end end.must_raise ArgumentError From aeb16a7bb420c8a18595dba64cffa0d5af00127b Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Fri, 28 Jul 2017 01:15:16 -0400 Subject: [PATCH 5/6] Remove unnecessary trailing comma in arg list --- lib/retriable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/retriable.rb b/lib/retriable.rb index 48ed279..04644ec 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -48,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 From de3fcb8c8c70f213edacf07d481e010e9eb4ff70 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Fri, 28 Jul 2017 01:34:08 -0400 Subject: [PATCH 6/6] Add available contexts in the exception message when calling a context key that doesn't exist --- lib/retriable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/retriable.rb b/lib/retriable.rb index 04644ec..01fffe6 100644 --- a/lib/retriable.rb +++ b/lib/retriable.rb @@ -16,7 +16,7 @@ def config def with_context(context_key, options = {}, &block) if !config.contexts.key?(context_key) - raise ArgumentError, "#{context_key} not found in Retriable.config.contexts" + 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