spec/retriable_spec.rb in retriable-3.1.1 vs spec/retriable_spec.rb in retriable-3.1.2

- old
+ new

@@ -1,434 +1,265 @@ -require_relative "spec_helper" +describe Retriable do + let(:time_table_handler) do + ->(_exception, try, _elapsed_time, next_interval) { @next_interval_table[try] = next_interval } + end -class TestError < Exception; end + before(:each) do + described_class.configure { |c| c.sleep_disabled = true } + @tries = 0 + @next_interval_table = {} + end -describe Retriable do - subject do - Retriable + def increment_tries + @tries += 1 end - before do - srand 0 + def increment_tries_with_exception(exception_class = nil) + exception_class ||= StandardError + increment_tries + raise exception_class, "#{exception_class} occurred" end - describe "with sleep disabled" do - before do - Retriable.configure do |c| - c.sleep_disabled = true - end + context "global scope extension" do + it "cannot be called in the global scope without requiring the core_ext/kernel" do + expect { retriable { puts "should raise NoMethodError" } }.to raise_error(NoMethodError) end - it "stops at first try if the block does not raise an exception" do - tries = 0 - subject.retriable do - tries += 1 - end + it "can be called once the kernel extension is required" do + require_relative "../lib/retriable/core_ext/kernel" - expect(tries).must_equal 1 + expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError) + expect(@tries).to eq(3) end + end - it "raises a LocalJumpError if #retriable is not given a block" do - expect do - subject.retriable on: StandardError - end.must_raise LocalJumpError + context "#retriable" do + it "raises a LocalJumpError if not given a block" do + expect { described_class.retriable }.to raise_error(LocalJumpError) + expect { described_class.retriable(timeout: 2) }.to raise_error(LocalJumpError) + end - expect do - subject.retriable on: StandardError, timeout: 2 - end.must_raise LocalJumpError + it "stops at first try if the block does not raise an exception" do + described_class.retriable { increment_tries } + expect(@tries).to eq(1) end it "makes 3 tries when retrying block of code raising StandardError with no arguments" do - tries = 0 - - expect do - subject.retriable do - tries += 1 - raise StandardError.new, "StandardError occurred" - end - end.must_raise StandardError - - expect(tries).must_equal 3 + expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError) + expect(@tries).to eq(3) end - it "makes only 1 try when exception raised is not ancestor of StandardError" do - tries = 0 - + it "makes only 1 try when exception raised is not descendent of StandardError" do expect do - subject.retriable do - tries += 1 - raise TestError.new, "TestError occurred" - end - end.must_raise TestError + described_class.retriable { increment_tries_with_exception(NonStandardError) } + end.to raise_error(NonStandardError) - expect(tries).must_equal 1 + expect(@tries).to eq(1) end - it "#retriable with custom exception tries 3 times and re-raises the exception" do - tries = 0 - + it "with custom exception tries 3 times and re-raises the exception" do expect do - subject.retriable on: TestError do - tries += 1 - raise TestError.new, "TestError occurred" - end - end.must_raise TestError + described_class.retriable(on: NonStandardError) { increment_tries_with_exception(NonStandardError) } + end.to raise_error(NonStandardError) - expect(tries).must_equal 3 + expect(@tries).to eq(3) end - it "#retriable tries 10 times" do - tries = 0 - - expect do - subject.retriable(tries: 10) do - tries += 1 - raise StandardError.new, "StandardError occurred" - end - end.must_raise StandardError - - expect(tries).must_equal 10 + it "tries 10 times when specified" do + expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError) + expect(@tries).to eq(10) end - it "#retriable will timeout after 1 second" do - expect do - subject.retriable timeout: 1 do - sleep 1.1 - end - end.must_raise Timeout::Error + it "will timeout after 1 second" do + expect { described_class.retriable(timeout: 1) { sleep(1.1) } }.to raise_error(Timeout::Error) end it "applies a randomized exponential backoff to each try" do - tries = 0 - time_table = [] - - handler = lambda do |exception, _try, _elapsed_time, next_interval| - expect(exception.class).must_equal ArgumentError - time_table << next_interval - end - expect do - Retriable.retriable( - on: [EOFError, ArgumentError], - on_retry: handler, - tries: 10, - ) do - tries += 1 - raise ArgumentError.new, "ArgumentError occurred" - end - end.must_raise ArgumentError + described_class.retriable(on_retry: time_table_handler, tries: 10) { increment_tries_with_exception } + end.to raise_error(StandardError) - expect(time_table).must_equal([ - 0.5244067512211441, - 0.9113920238761231, - 1.2406087918999114, - 1.7632403621664823, - 2.338001204738311, - 4.350816718580626, - 5.339852157217869, - 11.889873261212443, - 18.756037881636484, - nil, - ]) + expect(@next_interval_table).to eq( + 1 => 0.5244067512211441, + 2 => 0.9113920238761231, + 3 => 1.2406087918999114, + 4 => 1.7632403621664823, + 5 => 2.338001204738311, + 6 => 4.350816718580626, + 7 => 5.339852157217869, + 8 => 11.889873261212443, + 9 => 18.756037881636484, + 10 => nil, + ) - expect(tries).must_equal(10) + expect(@tries).to eq(10) end - describe "retries with an on_#retriable handler, 6 max retries, and a 0.0 rand_factor" do - before do - tries = 6 - @try_count = 0 - @time_table = {} + context "with rand_factor 0.0 and an on_retry handler" do + let(:tries) { 6 } + let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } } + let(:args) { { on_retry: time_table_handler, rand_factor: 0.0, tries: tries } } - handler = lambda do |exception, try, _elapsed_time, next_interval| - expect(exception.class).must_equal ArgumentError - @time_table[try] = next_interval + it "applies a non-randomized exponential backoff to each try" do + described_class.retriable(args) do + increment_tries + raise StandardError if @tries < tries end - Retriable.retriable( - on: [EOFError, ArgumentError], - on_retry: handler, - rand_factor: 0.0, - tries: tries, - ) do - @try_count += 1 - raise ArgumentError.new, "ArgumentError occurred" if @try_count < tries - end + expect(@tries).to eq(tries) + expect(@next_interval_table).to eq(no_rand_timetable.merge(4 => 1.6875, 5 => 2.53125)) end - it "makes 6 tries" do - expect(@try_count).must_equal 6 - end + it "obeys a max interval of 1.5 seconds" do + expect do + described_class.retriable(args.merge(max_interval: 1.5)) { increment_tries_with_exception } + end.to raise_error(StandardError) - it "applies a non-randomized exponential backoff to each try" do - expect(@time_table).must_equal( - 1 => 0.5, - 2 => 0.75, - 3 => 1.125, - 4 => 1.6875, - 5 => 2.53125, - ) + expect(@next_interval_table).to eq(no_rand_timetable.merge(4 => 1.5, 5 => 1.5, 6 => nil)) end - end - it "#retriable has a max interval of 1.5 seconds" do - tries = 0 - time_table = {} + it "obeys custom defined intervals" do + interval_hash = no_rand_timetable.merge(4 => 1.5, 5 => 1.5, 6 => nil) + intervals = interval_hash.values.compact.sort - handler = lambda do |_exception, try, _elapsed_time, next_interval| - time_table[try] = next_interval - end + expect do + described_class.retriable(on_retry: time_table_handler, intervals: intervals) do + increment_tries_with_exception + end + end.to raise_error(StandardError) - expect do - subject.retriable( - on: StandardError, - on_retry: handler, - rand_factor: 0.0, - tries: 5, - max_interval: 1.5, - ) do - tries += 1 - raise StandardError.new, "StandardError occurred" - end - end.must_raise StandardError - - expect(time_table).must_equal( - 1 => 0.5, - 2 => 0.75, - 3 => 1.125, - 4 => 1.5, - 5 => nil, - ) + expect(@next_interval_table).to eq(interval_hash) + expect(@tries).to eq(intervals.size + 1) + end end - it "#retriable with custom defined intervals" do - intervals = [ - 0.5, - 0.75, - 1.125, - 1.5, - 1.5, - ] - time_table = {} + context "with an array :on parameter" do + it "handles both kinds of exceptions" do + described_class.retriable(on: [StandardError, NonStandardError]) do + increment_tries - handler = lambda do |_exception, try, _elapsed_time, next_interval| - time_table[try] = next_interval - end - - try_count = 0 - - expect do - subject.retriable( - on_retry: handler, - intervals: intervals, - ) do - try_count += 1 - raise StandardError.new, "StandardError occurred" + raise StandardError if @tries == 1 + raise NonStandardError if @tries == 2 end - end.must_raise StandardError - expect(time_table).must_equal( - 1 => 0.5, - 2 => 0.75, - 3 => 1.125, - 4 => 1.5, - 5 => 1.5, - 6 => nil, - ) - - expect(try_count).must_equal(6) + expect(@tries).to eq(3) + end end - it "#retriable with a hash exception where the value is an exception message pattern" do - e = expect do - subject.retriable on: { TestError => /something went wrong/ } do - raise TestError, "something went wrong" - end - end.must_raise TestError + context "with a hash :on parameter" do + let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } } - expect(e.message).must_equal "something went wrong" - end + it "where the value is an exception message pattern" do + expect do + described_class.retriable(on: on_hash) { increment_tries_with_exception(NonStandardError) } + end.to raise_error(NonStandardError, /NonStandardError occurred/) - it "#retriable with a hash exception list matches exception subclasses" do - class SecondTestError < TestError; end - class DifferentTestError < Exception; end + expect(@tries).to eq(3) + end - tries = 0 - e = expect do - subject.retriable on: { - DifferentTestError => /should never happen/, - TestError => /something went wrong/, - DifferentTestError => /also should never happen/, - }, tries: 4 do - tries += 1 - raise SecondTestError, "something went wrong" - end - end.must_raise SecondTestError + it "matches exception subclasses when message matches pattern" do + expect do + described_class.retriable(on: on_hash.merge(DifferentError => [/shouldn't happen/, /also not/])) do + increment_tries_with_exception(SecondNonStandardError) + end + end.to raise_error(SecondNonStandardError, /SecondNonStandardError occurred/) - expect(e.message).must_equal "something went wrong" - expect(tries).must_equal 4 - end + expect(@tries).to eq(3) + end - it "#retriable with a hash exception list does not retry matching exception subclass but not message" do - class SecondTestError < TestError; end + it "does not retry matching exception subclass but not message" do + expect do + described_class.retriable(on: on_hash) do + increment_tries + raise SecondNonStandardError, "not a match" + end + end.to raise_error(SecondNonStandardError, /not a match/) - tries = 0 - expect do - subject.retriable on: { TestError => /something went wrong/ }, tries: 4 do - tries += 1 - raise SecondTestError, "not a match" - end - end.must_raise SecondTestError + expect(@tries).to eq(1) + end - expect(tries).must_equal 1 - end + it "successfully retries when the values are arrays of exception message patterns" do + exceptions = [] + handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception } + on_hash = { StandardError => nil, NonStandardError => [/foo/, /bar/] } - it "#retriable with a hash exception list where the values are exception message patterns" do - tries = 0 - exceptions = [] - handler = lambda do |exception, try, _elapsed_time, _next_interval| - exceptions[try] = exception - end + expect do + described_class.retriable(tries: 4, on: on_hash, on_retry: handler) do + increment_tries - e = expect do - subject.retriable tries: 4, on: { StandardError => nil, TestError => [/foo/, /bar/] }, on_retry: handler do - tries += 1 - case tries - when 1 - raise TestError, "foo" - when 2 - raise TestError, "bar" - when 3 - raise StandardError - else - raise TestError, "crash" + case @tries + when 1 + raise NonStandardError, "foo" + when 2 + raise NonStandardError, "bar" + when 3 + raise StandardError + else + raise NonStandardError, "crash" + end end - end - end.must_raise TestError + end.to raise_error(NonStandardError, /crash/) - expect(e.message).must_equal "crash" - expect(exceptions[1].class).must_equal TestError - expect(exceptions[1].message).must_equal "foo" - expect(exceptions[2].class).must_equal TestError - expect(exceptions[2].message).must_equal "bar" - expect(exceptions[3].class).must_equal StandardError + expect(exceptions[1]).to be_a(NonStandardError) + expect(exceptions[1].message).to eq("foo") + expect(exceptions[2]).to be_a(NonStandardError) + expect(exceptions[2].message).to eq("bar") + expect(exceptions[3]).to be_a(StandardError) + end end - it "#retriable can be called in the global scope" do - expect do - retriable do - puts "should raise NoMethodError" - end - end.must_raise NoMethodError + it "runs for a max elapsed time of 2 seconds" do + described_class.configure { |c| c.sleep_disabled = false } - require_relative "../lib/retriable/core_ext/kernel" - - tries = 0 - expect do - retriable do - tries += 1 - raise StandardError + described_class.retriable(base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0, max_elapsed_time: 2.0) do + increment_tries_with_exception end - end.must_raise StandardError + end.to raise_error(StandardError) - expect(tries).must_equal 3 + expect(@tries).to eq(2) end - end - it "#retriable runs for a max elapsed time of 2 seconds" do - subject.configure do |c| - c.sleep_disabled = false + it "raises ArgumentError on invalid options" do + expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError) end - - expect(subject.config.sleep_disabled).must_equal false - - tries = 0 - time_table = {} - - handler = lambda do |_exception, try, elapsed_time, _next_interval| - time_table[try] = elapsed_time - end - - expect do - subject.retriable( - base_interval: 1.0, - multiplier: 1.0, - rand_factor: 0.0, - max_elapsed_time: 2.0, - on_retry: handler, - ) do - tries += 1 - raise EOFError - end - end.must_raise EOFError - - expect(tries).must_equal 2 end - it "raises NoMethodError on invalid configuration" do - assert_raises NoMethodError do - Retriable.configure { |c| c.does_not_exist = 123 } + context "#configure" do + it "raises NoMethodError on invalid configuration" do + expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError) end end - it "raises ArgumentError on invalid option on #retriable" do - assert_raises ArgumentError do - Retriable.retriable(does_not_exist: 123) - end - end + context "#with_context" do + let(:api_tries) { 4 } - describe "#with_context" do before do - Retriable.configure do |c| - c.sleep_disabled = true + described_class.configure do |c| c.contexts[:sql] = { tries: 1 } - c.contexts[:api] = { tries: 3 } + c.contexts[:api] = { tries: api_tries } 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 + it "stops at first try if the block does not raise an exception" do + described_class.with_context(:sql) { increment_tries } + expect(@tries).to eq(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 + it "respects the context options" do + expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError) + expect(@tries).to eq(api_tries) end - it "with_context allows override options" do - tries = 0 - + it "allows override options" do expect do - subject.with_context(:sql, tries: 5) do - tries += 1 - raise StandardError.new, "StandardError occurred" - end - end.must_raise StandardError + described_class.with_context(:sql, tries: 5) { increment_tries_with_exception } + end.to raise_error(StandardError) - expect(tries).must_equal 5 + expect(@tries).to eq(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 + expect { described_class.with_context(:wtf) { increment_tries } }.to raise_error(ArgumentError, /wtf not found/) end end end