Skip to content

Commit

Permalink
added retry_intervals option
Browse files Browse the repository at this point in the history
  • Loading branch information
Roy Razon committed Dec 27, 2018
1 parent b008d9a commit 83c0fbd
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 20 deletions.
1 change: 1 addition & 0 deletions hub_client.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rspec"
spec.add_development_dependency "webmock"
spec.add_development_dependency "pry"
spec.add_development_dependency 'pry-byebug'
end
23 changes: 14 additions & 9 deletions lib/hub_client.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "hub_client/exponential_backoff_interval"
require "hub_client/configuration"
require "hub_client/logger"
require "hub_client/version"
Expand All @@ -13,8 +14,19 @@ def self.publish(metadata, content, env = nil)

hub_url = build_hub_url(HubClient.configuration.endpoint_url)

RestClient.post(hub_url, payload.to_json, content_type: :json, accept: :json) do |response, request, result|
handle_response(response, request, result)
retry_intervals = HubClient.configuration.retry_intervals

retries = 0
begin
RestClient.post(hub_url, payload.to_json, content_type: :json, accept: :json)
rescue RestClient::Exception => e
HubClient.logger.warn("HubClient Exception #{e.class}: #{e.message} Code: #{e.http_code} Response: #{e.response} Request: #{payload}")

retries += 1
sleep_interval = retry_intervals && retry_intervals.next(retries)
raise unless sleep_interval
Kernel.sleep(sleep_interval)
retry
end
end

Expand All @@ -25,12 +37,5 @@ def self.build_hub_url(endpoint_url)
"#{endpoint_url}/api/#{HUB_VERSION}/messages"
end

def self.handle_response(response, request, result)
# When request didn't succeed we log it
unless result.code.start_with?("2")
HubClient.logger.info("HubClient Code: #{result.code} Response: #{response} Request: #{request.args}")
end
end

class ConfigArgumentMissing < StandardError; end
end
2 changes: 1 addition & 1 deletion lib/hub_client/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module HubClient
class Configuration
attr_accessor :env, :access_token, :endpoint_url
attr_accessor :env, :access_token, :endpoint_url, :retry_intervals
end

def self.configuration
Expand Down
27 changes: 27 additions & 0 deletions lib/hub_client/exponential_backoff_interval.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module HubClient
class ExponentialBackoffInterval
attr_accessor :initial, :multiplier, :rand_factor, :max_count

DEFAULT_OPTS = {
initial: 0.5,
multiplier: 1.2,
rand_factor: 0.05,
max_count: 10,
}

def initialize(opts = {})
opts = DEFAULT_OPTS.merge(opts)
@initial = opts[:initial]
@multiplier = opts[:multiplier]
@rand_factor = opts[:rand_factor]
@max_count = opts[:max_count]
end

def next(count)
return nil if count > max_count
result = @initial * (@multiplier ** count)
r = Kernel.rand(-@rand_factor..@rand_factor)
result + result * r
end
end
end
1 change: 1 addition & 0 deletions lib/hub_client/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ def method_missing(method_sym, *args, &block)

def self.logger
@logger ||= Logger.new
@logger
end
end
20 changes: 20 additions & 0 deletions spec/exponential_backoff_interval_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'spec_helper'
require "hub_client/exponential_backoff_interval"

describe HubClient::ExponentialBackoffInterval do
let(:default_opts) {described_class.const_get(:DEFAULT_OPTS)}

describe '#next' do
subject {described_class.new(initial: 12, max_count: 3)}

context 'when called with 2' do
it 'returns a correct value' do
multiplier = default_opts[:multiplier]
med = 12 * (multiplier ** 2)
rand_factor = default_opts[:rand_factor]
expect(Kernel).to receive(:rand).with(-rand_factor..rand_factor) {0.1}
expect(subject.next(2)).to eq(med + 0.1*med)
end
end
end
end
161 changes: 151 additions & 10 deletions spec/hub_client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
require 'hub_client'

describe HubClient do
before do
HubClient.configure {|config| config.retry_intervals = nil}
end

describe "#publish" do
context "not configured" do
after(:each) { HubClient.reset_configuration }
Expand Down Expand Up @@ -29,16 +33,6 @@
assert_requested :post, HubClient.build_hub_url(HubClient.configuration.endpoint_url)
end

it "logs the request when hub didn't return success code" do
stub_request(:post, HubClient.build_hub_url(HubClient.configuration.endpoint_url)).
with(body: { env: "il-qa2", type: "order_created", content: { some: "content" } }).
to_return(status: 500)

expect(HubClient.logger).to receive(:info)
HubClient.publish(:order_created, { some: "content" })
assert_requested :post, HubClient.build_hub_url(HubClient.configuration.endpoint_url)
end

it "overrides the env when supplied" do
stub_request(:post, HubClient.build_hub_url(HubClient.configuration.endpoint_url)).
with(body: { env: "batman-env", type: "order_created", content: { some: "content" } }).
Expand Down Expand Up @@ -67,6 +61,153 @@
end

after(:all) { HubClient.reset_configuration }

describe 'exception handling' do
def action
HubClient.publish(:order_created, { some: "content" })
end

def stub_publish_request
@net_response = stub_request(:post, HubClient.build_hub_url(HubClient.configuration.endpoint_url)).
with(body: { env: "il-qa2", type: "order_created", content: { some: "content" } })
end

shared_examples_for 'raises the exception' do |exception_class|
it 'raises the exception' do
expect {action}.to raise_error(exception_class)
end

it 'logs the exception' do
logger = double('logger', warn: nil)
allow(HubClient).to receive(:logger).and_return logger
expect(logger).to receive(:warn).with(/#{exception_class}/)
action rescue nil
end
end

context 'when no retry_intervals is configured (default)' do
context 'when a timeout error occurs' do
before do
stub_publish_request.to_timeout
end

it_behaves_like 'raises the exception', RestClient::RequestTimeout
end

context 'when a HTTP errors occurs' do
before do
stub_publish_request.to_return(status: 500)
end

it_behaves_like 'raises the exception', RestClient::InternalServerError
end
end

context 'when retry_interval is configured' do
before do
@my_retry_interval = double('my_retry_interval')

HubClient.configure do |config|
config.retry_intervals = @my_retry_interval
end
end

context 'when a timeout error occurs' do
before do
stub_publish_request.to_timeout
end

context 'when the retry_interval.next method returns nil' do
before do
allow(@my_retry_interval).to receive(:next).with(1).and_return(nil)
end

it_behaves_like 'raises the exception', RestClient::RequestTimeout

it 'does not sleep' do
expect(Kernel).not_to receive(:sleep)
action rescue nil
end
end

context 'when the retry_interval.next method returns a number' do
before do
expect(@my_retry_interval).to receive(:next).with(1).and_return(12)
allow(Kernel).to receive(:sleep)
end

context 'and the next network call succeeds' do
before do
@net_response.then.to_return({body: 'ok'})
end

it 'sleeps for the returned number' do
action
expect(Kernel).to have_received(:sleep).with(12).once
end

it 'does not raise an exception' do
action
end
end

context 'and the next network call times out again' do
before do
@net_response.then.to_timeout
end

context 'and the the retry_interval.next call returns nil' do
before do
expect(@my_retry_interval).to receive(:next).with(2).and_return(nil)
end

it_behaves_like 'raises the exception', RestClient::RequestTimeout
end

context 'and the the retry_interval.next call returns a number' do
before do
expect(@my_retry_interval).to receive(:next).with(2).and_return(13)
end

context 'and the next network call succeeds' do
before do
@net_response.then.to_return({body: 'ok'})
end

it 'sleeps for the returned number' do
action
expect(Kernel).to have_received(:sleep).with(13).once
end

it 'does not raise an exception' do
action
end
end
end
end
end
end

context 'when a HTTP errors occurs' do
before do
stub_publish_request.to_return(status: 500)
end

context 'when the retry_interval.next method returns nil' do
before do
allow(@my_retry_interval).to receive(:next).with(1).and_return(nil)
end

it_behaves_like 'raises the exception', RestClient::InternalServerError

it 'does not sleep' do
expect(Kernel).not_to receive(:sleep)
action rescue nil
end
end
end
end
end
end
end

Expand Down

0 comments on commit 83c0fbd

Please sign in to comment.