Skip to content

Commit

Permalink
Add retry mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
phylor committed Aug 16, 2023
1 parent 8b3c18d commit 76ea8d8
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 19 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ and api_client_secret. You can setup these ENV variables:
ENV['IOKI_OAUTH_APP_ID']
ENV['IOKI_OAUTH_APP_SECRET']
ENV['IOKI_OAUTH_APP_URL']
ENV['IOKI_RETRY_COUNT']
ENV['IOKI_RETRY_SLEEP_SECONDS']
```

or define them for one of the three supported apis with a prefix:
Expand Down
16 changes: 9 additions & 7 deletions lib/ioki/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ def initialize(config, api)
options = args.last.is_a?(::Hash) ? args.pop : {}
model = args.last

Ioki::Oauth::WithTokenRefresh.call(config) do
if [Endpoints::Create, Endpoints::Update, Endpoints::UpdateSingular].include?(endpoint.class)
endpoint.call(self, model, args, options)
elsif endpoint.is_a? Endpoints::Index
endpoint.call(self, args, options, &block)
else
endpoint.call(self, args, options)
Ioki::Retry.n_times(config.retry_count, config.retry_sleep_seconds) do
Ioki::Oauth::WithTokenRefresh.call(config) do
if [Endpoints::Create, Endpoints::Update, Endpoints::UpdateSingular].include?(endpoint.class)
endpoint.call(self, model, args, options)
elsif endpoint.is_a? Endpoints::Index
endpoint.call(self, args, options, &block)
else
endpoint.call(self, args, options)
end
end
end
end
Expand Down
22 changes: 15 additions & 7 deletions lib/ioki/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ module Ioki
class Configuration
DEFAULT_VALUES =
{
api_base_url: 'https://app.io.ki/api/',
api_version: '20210101',
api_bleeding_edge: false,
language: 'de',
logger_options: { headers: true, bodies: false, log_level: :info }
api_base_url: 'https://app.io.ki/api/',
api_version: '20210101',
api_bleeding_edge: false,
language: 'de',
logger_options: { headers: true, bodies: false, log_level: :info },
retry_count: 3,
retry_sleep_seconds: 1
}.freeze

CONFIG_KEYS = [
Expand All @@ -28,7 +30,9 @@ class Configuration
:oauth_app_url,
:oauth_access_token,
:oauth_refresh_token,
:oauth_token_callback
:oauth_token_callback,
:retry_count,
:retry_sleep_seconds
].freeze

attr_accessor(*CONFIG_KEYS)
Expand All @@ -52,6 +56,8 @@ def initialize(params = {})
@oauth_access_token = params[:oauth_access_token]
@oauth_refresh_token = params[:oauth_refresh_token]
@oauth_token_callback = params[:oauth_token_callback]
@retry_count = params[:retry_count]
@retry_sleep_seconds = params[:retry_sleep_seconds]
# you can pass in a custom Faraday::Connection instance:
@http_adapter = params[:http_adapter] || Ioki::HttpAdapter.get(self)
@custom_http_adapter = !!params[:http_adapter]
Expand Down Expand Up @@ -81,7 +87,9 @@ def self.values_from_env(env_prefix = '')
api_bleeding_edge: ENV.fetch("#{prefix}_API_BLEEDING_EDGE", nil)&.downcase == 'true',
oauth_app_id: ENV.fetch("#{prefix}_OAUTH_APP_ID", nil),
oauth_app_secret: ENV.fetch("#{prefix}_OAUTH_APP_SECRET", nil),
oauth_app_url: ENV.fetch("#{prefix}_OAUTH_APP_URL", nil)
oauth_app_url: ENV.fetch("#{prefix}_OAUTH_APP_URL", nil),
retry_count: ENV.fetch("#{prefix}_RETRY_COUNT", nil),
retry_sleep_seconds: ENV.fetch("#{prefix}_RETRY_SLEEP_SECONDS", nil)
}.reject { |_key, value| value.nil? || value.to_s == '' }
end

Expand Down
24 changes: 24 additions & 0 deletions lib/ioki/retry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Ioki
module Retry
class MaximumReached < ::StandardError; end

def self.n_times(max_retries, sleep_seconds = 1)
retries = max_retries

begin
yield
rescue Ioki::Error::OauthRefreshToken => e
raise e
rescue StandardError => e
retries -= 1

raise MaximumReached, "Gave up after #{max_retries} retries: #{e.message}" if retries <= 0

sleep(sleep_seconds)
retry
end
end
end
end
8 changes: 6 additions & 2 deletions spec/ioki/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
:oauth_app_url,
:oauth_access_token,
:oauth_refresh_token,
:oauth_token_callback
:oauth_token_callback,
:retry_count,
:retry_sleep_seconds
)
end

Expand All @@ -60,7 +62,9 @@
api_token: nil,
api_bleeding_edge: false,
language: 'de',
logger_options: described_class::DEFAULT_VALUES[:logger_options]
logger_options: described_class::DEFAULT_VALUES[:logger_options],
retry_count: 3,
retry_sleep_seconds: 1
}.freeze
)
end
Expand Down
4 changes: 3 additions & 1 deletion spec/ioki/driver_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
api_client_secret: 'SECRET',
api_client_version: 'VERSION',
api_token: 'TOKEN',
language: 'de'
language: 'de',
retry_count: 1,
retry_sleep_seconds: 1
),
described_class
)
Expand Down
4 changes: 3 additions & 1 deletion spec/ioki/operator_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
api_client_secret: 'SECRET',
api_client_version: 'VERSION',
api_token: 'TOKEN',
language: 'de'
language: 'de',
retry_count: 1,
retry_sleep_seconds: 1
),
described_class
)
Expand Down
4 changes: 3 additions & 1 deletion spec/ioki/platform_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
api_client_secret: 'SECRET',
api_client_version: 'VERSION',
api_token: 'TOKEN',
language: 'de'
language: 'de',
retry_count: 1,
retry_sleep_seconds: 1
),
described_class
)
Expand Down
98 changes: 98 additions & 0 deletions spec/ioki/retry_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

require 'spec_helper'
require 'ioki/apis/endpoints/endpoints'
require 'ioki/retry'
require 'webmock'

class DummyApi
ENDPOINTS =
[
Ioki::Endpoints::Index.new(
:ping,
base_path: ['driver'],
model_class: Ioki::Model::Base
)
].freeze
end

RSpec.describe Ioki::Retry do
include WebMock::API

it 'executes the given block' do
called = false

described_class.n_times(3) do
called = true
end

expect(called).to be true
end

it 'retries when an exception is raised' do
first_run = true
called_count = 0

described_class.n_times(3) do
called_count += 1

if first_run
first_run = false
raise Ioki::Error::Unauthorized, nil
end
end

expect(called_count).to eq 2
end

it 'retries only to the maximum' do
called_count = 0

expect do
described_class.n_times(3) do
called_count += 1

raise Ioki::Error::Unauthorized, nil
end
end.to raise_error(Ioki::Retry::MaximumReached)

expect(called_count).to eq 3
end

context 'with a Ioki client' do
let(:client) { Ioki::Client.new(Ioki::Configuration.new, DummyApi) }

before do
WebMock.enable!
VCR.turn_off!
end

after do
WebMock.disable!
VCR.turn_on!
end

it 'retries failed requests' do
ping_request = stub_request(:get, 'https://app.io.ki/api/driver/ping')
.to_return(
{ status: 500, body: '{"data": []}', headers: { content_type: 'application/json' } },
{ status: 200, body: '{"data": []}', headers: { content_type: 'application/json' } }
)

client.ping

expect(ping_request).to have_been_requested.twice
end

it 'raises after max retries' do
ping_request = stub_request(:get, 'https://app.io.ki/api/driver/ping')
.to_return(status: 500, body: '{"data": []}', headers: { content_type: 'application/json' })

expect do
client.ping
end.to raise_error Ioki::Retry::MaximumReached

expect(ping_request).to have_been_requested.times(3)
end
end
end

0 comments on commit 76ea8d8

Please sign in to comment.