aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRobert2017-07-26 19:47:26 +0200
committerRobert2017-07-26 21:54:50 +0200
commit9b97b8728a1d3714147591e4facacad097f2a155 (patch)
treec1abc3d6c0431a9af27bf76ffc6c59944b1fe903
parentd385a6da731dc6b3ec0ec5bec1e94eb2dcff5efb (diff)
downloadchouette-core-9b97b8728a1d3714147591e4facacad097f2a155.tar.bz2
Refs: #3507@1.5h; RetryService speced and implmented
-rw-r--r--app/services/retry_service.rb39
-rw-r--r--spec/services/retry_service_spec.rb112
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