diff options
| author | Robert | 2017-07-26 19:47:26 +0200 |
|---|---|---|
| committer | Robert | 2017-07-26 21:54:50 +0200 |
| commit | 9b97b8728a1d3714147591e4facacad097f2a155 (patch) | |
| tree | c1abc3d6c0431a9af27bf76ffc6c59944b1fe903 | |
| parent | d385a6da731dc6b3ec0ec5bec1e94eb2dcff5efb (diff) | |
| download | chouette-core-9b97b8728a1d3714147591e4facacad097f2a155.tar.bz2 | |
Refs: #3507@1.5h; RetryService speced and implmented
| -rw-r--r-- | app/services/retry_service.rb | 39 | ||||
| -rw-r--r-- | spec/services/retry_service_spec.rb | 112 |
2 files changed, 151 insertions, 0 deletions
diff --git a/app/services/retry_service.rb b/app/services/retry_service.rb new file mode 100644 index 000000000..0b8f5447b --- /dev/null +++ b/app/services/retry_service.rb @@ -0,0 +1,39 @@ +class RetryService + + Retry = Class.new(RuntimeError) + + def initialize( delays:, rescue_from: [], &blk ) + @intervals = delays + @registered_exceptions = Array(rescue_from) << Retry + @failure_callback = blk + end + + def execute &blk + status, result = execute_protected blk + return [status, result] if status == :ok + @intervals.each do | interval | + @failure_callback.try(:call) + sleep interval + status, result = execute_protected blk + return [status, result] if status == :ok + end + [status, result] + end + + def register_failure_callback &blk + @failure_callback = blk + end + + + private + + def execute_protected blk + [:ok, blk.()] + rescue Exception => e + if @registered_exceptions.any?{ |re| e.is_a? re } + [:error, e] + else + raise + end + end +end diff --git a/spec/services/retry_service_spec.rb b/spec/services/retry_service_spec.rb new file mode 100644 index 000000000..93788c9ae --- /dev/null +++ b/spec/services/retry_service_spec.rb @@ -0,0 +1,112 @@ +RSpec.describe RetryService do + subject { described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] } + + context 'no retry necessary' do + before do + expect( subject ).not_to receive(:sleep) + end + + it 'returns a tuple :ok and the result' do + expect( subject.execute { 42 } ).to eq([:ok, 42]) + end + it 'does not fail on nil' do + expect( subject.execute { nil } ).to eq([:ok, nil]) + end + + it 'fails wihout retries if raising un unregistered exception' do + expect{ subject.execute{ raise KeyError } }.to raise_error(KeyError) + end + + end + + context 'all retries fail' do + before do + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + end + it 'fails after raising a registered exception n times' do + result = subject.execute{ raise ArgumentError } + expect( result.first ).to eq(:error) + expect( result.last ).to be_kind_of(ArgumentError) + end + it 'fails with an explicit try again (automatically registered exception)' do + result = subject.execute{ raise RetryService::Retry } + expect( result.first ).to eq(:error) + expect( result.last ).to be_kind_of(RetryService::Retry) + end + end + + context "if at first you don't succeed" do + before do + @count = 0 + expect( subject ).to receive(:sleep).with(2) + end + + it 'succeds the second time' do + expect( subject.execute{ succeed_later(ArgumentError){ 42 } } ).to eq([:ok, 42]) + end + + it 'succeeds the second time with try again (automatically registered exception)' do + expect( subject.execute{ succeed_later(RetryService::Retry){ 42 } } ).to eq([:ok, 42]) + end + end + + context 'last chance' do + before do + @count = 0 + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + end + it 'succeeds the third time with try again (automatically registered exception)' do + expect( subject.execute{ succeed_later(RetryService::Retry, count: 2){ 42 } } ).to eq([:ok, 42]) + end + end + + context 'failure callback once' do + before do + @failures = 0 + @count = 0 + expect( subject ).to receive(:sleep).with(2) + subject.register_failure_callback { @failures += 1 } + end + it 'succeeds the second time and calls the failure_callback once' do + subject.execute{ succeed_later(RetryService::Retry){ 42 } } + expect( @failures ).to eq(1) + end + end + + context 'failure callback twice' do + before do + @failures = 0 + @count = 0 + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + subject.register_failure_callback { @failures += 1 } + end + it 'succeeds the third time and calls the failure_callback twice' do + subject.execute{ succeed_later(NameError, count: 2){ 42 } } + expect( @failures ).to eq(2) + end + end + + context 'failure callback in constructor' do + subject do + described_class.new(delays: [1]){ @failures += 1} + end + before do + @failures = 0 + @count = 0 + expect( subject ).to receive(:sleep).with(1) + end + it 'succeeds the second time and calls the failure_callback once' do + subject.execute{ succeed_later(RetryService::Retry){ 42 } } + expect( @failures ).to eq(1) + end + end + + def succeed_later error, count: 1, &blk + return blk.() unless @count < count + @count += 1 + raise error, 'error' + end +end |
