mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Use faraday retry, move retry logic to concrete provider level (#2042)
This commit is contained in:
parent
0a17b84566
commit
939244bd3e
5 changed files with 50 additions and 90 deletions
|
@ -169,7 +169,7 @@ GEM
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (3.4.0)
|
faraday-net_http (3.4.0)
|
||||||
net-http (>= 0.5.0)
|
net-http (>= 0.5.0)
|
||||||
faraday-retry (2.2.1)
|
faraday-retry (2.3.0)
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
ffi (1.17.1-aarch64-linux-gnu)
|
ffi (1.17.1-aarch64-linux-gnu)
|
||||||
ffi (1.17.1-aarch64-linux-musl)
|
ffi (1.17.1-aarch64-linux-musl)
|
||||||
|
@ -239,7 +239,7 @@ GEM
|
||||||
listen (3.9.0)
|
listen (3.9.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
logger (1.6.6)
|
logger (1.7.0)
|
||||||
logtail (0.1.17)
|
logtail (0.1.17)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
logtail-rack (0.2.6)
|
logtail-rack (0.2.6)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class Provider
|
class Provider
|
||||||
include Retryable
|
|
||||||
|
|
||||||
Response = Data.define(:success?, :data, :error)
|
Response = Data.define(:success?, :data, :error)
|
||||||
|
|
||||||
class Error < StandardError
|
class Error < StandardError
|
||||||
|
@ -23,17 +21,8 @@ class Provider
|
||||||
PaginatedData = Data.define(:paginated, :first_page, :total_pages)
|
PaginatedData = Data.define(:paginated, :first_page, :total_pages)
|
||||||
UsageData = Data.define(:used, :limit, :utilization, :plan)
|
UsageData = Data.define(:used, :limit, :utilization, :plan)
|
||||||
|
|
||||||
# Subclasses can specify errors that can be retried
|
def with_provider_response(error_transformer: nil, &block)
|
||||||
def retryable_errors
|
data = yield
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_provider_response(retries: default_retries, error_transformer: nil, &block)
|
|
||||||
data = if retries > 0
|
|
||||||
retrying(retryable_errors, max_retries: retries) { yield }
|
|
||||||
else
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
|
|
||||||
Response.new(
|
Response.new(
|
||||||
success?: true,
|
success?: true,
|
||||||
|
@ -67,9 +56,4 @@ class Provider
|
||||||
self.class::Error.new(error.message)
|
self.class::Error.new(error.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Override to set class-level number of retries for methods using `with_provider_response`
|
|
||||||
def default_retries
|
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Provider::Synth < Provider
|
||||||
# ================================
|
# ================================
|
||||||
|
|
||||||
def fetch_exchange_rate(from:, to:, date:)
|
def fetch_exchange_rate(from:, to:, date:)
|
||||||
with_provider_response retries: 2 do
|
with_provider_response do
|
||||||
response = client.get("#{base_url}/rates/historical") do |req|
|
response = client.get("#{base_url}/rates/historical") do |req|
|
||||||
req.params["date"] = date.to_s
|
req.params["date"] = date.to_s
|
||||||
req.params["from"] = from
|
req.params["from"] = from
|
||||||
|
@ -53,7 +53,7 @@ class Provider::Synth < Provider
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
|
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
|
||||||
with_provider_response retries: 1 do
|
with_provider_response do
|
||||||
data = paginate(
|
data = paginate(
|
||||||
"#{base_url}/rates/historical-range",
|
"#{base_url}/rates/historical-range",
|
||||||
from: from,
|
from: from,
|
||||||
|
@ -128,7 +128,7 @@ class Provider::Synth < Provider
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_security_prices(security, start_date:, end_date:)
|
def fetch_security_prices(security, start_date:, end_date:)
|
||||||
with_provider_response retries: 1 do
|
with_provider_response do
|
||||||
params = {
|
params = {
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
end_date: end_date
|
end_date: end_date
|
||||||
|
@ -191,14 +191,6 @@ class Provider::Synth < Provider
|
||||||
|
|
||||||
TransactionEnrichmentData = Data.define(:name, :icon_url, :category)
|
TransactionEnrichmentData = Data.define(:name, :icon_url, :category)
|
||||||
|
|
||||||
def retryable_errors
|
|
||||||
[
|
|
||||||
Faraday::TimeoutError,
|
|
||||||
Faraday::ConnectionFailed,
|
|
||||||
Faraday::SSLError
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
def base_url
|
def base_url
|
||||||
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
|
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
|
||||||
end
|
end
|
||||||
|
@ -213,6 +205,13 @@ class Provider::Synth < Provider
|
||||||
|
|
||||||
def client
|
def client
|
||||||
@client ||= Faraday.new(url: base_url) do |faraday|
|
@client ||= Faraday.new(url: base_url) do |faraday|
|
||||||
|
faraday.request(:retry, {
|
||||||
|
max: 2,
|
||||||
|
interval: 0.05,
|
||||||
|
interval_randomness: 0.5,
|
||||||
|
backoff_factor: 2
|
||||||
|
})
|
||||||
|
|
||||||
faraday.response :raise_error
|
faraday.response :raise_error
|
||||||
faraday.headers["Authorization"] = "Bearer #{api_key}"
|
faraday.headers["Authorization"] = "Bearer #{api_key}"
|
||||||
faraday.headers["X-Source"] = app_name
|
faraday.headers["X-Source"] = app_name
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
module Retryable
|
|
||||||
def retrying(retryable_errors = [], max_retries: 3)
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
begin
|
|
||||||
on_last_attempt = attempts == max_retries - 1
|
|
||||||
|
|
||||||
yield on_last_attempt
|
|
||||||
rescue *retryable_errors => e
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
if attempts < max_retries
|
|
||||||
retry
|
|
||||||
else
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -2,60 +2,56 @@ require "test_helper"
|
||||||
require "ostruct"
|
require "ostruct"
|
||||||
|
|
||||||
class TestProvider < Provider
|
class TestProvider < Provider
|
||||||
|
TestError = Class.new(StandardError)
|
||||||
|
|
||||||
|
def initialize(client)
|
||||||
|
@client = client
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_data
|
def fetch_data
|
||||||
with_provider_response(retries: 3) do
|
with_provider_response do
|
||||||
client.get("/test")
|
@client.get("/test")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def fetch_data_with_error_transformer
|
||||||
def client
|
with_provider_response(error_transformer: ->(error) { TestError.new(error.message) }) do
|
||||||
@client ||= Faraday.new
|
@client.get("/test")
|
||||||
end
|
|
||||||
|
|
||||||
def retryable_errors
|
|
||||||
[ Faraday::TimeoutError ]
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ProviderTest < ActiveSupport::TestCase
|
class ProviderTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@provider = TestProvider.new
|
@client = mock
|
||||||
|
@provider = TestProvider.new(@client)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "retries then provides failed response" do
|
test "returns success response with data" do
|
||||||
client = mock
|
@client.expects(:get).with("/test").returns({ some: "data" })
|
||||||
Faraday.stubs(:new).returns(client)
|
|
||||||
|
|
||||||
client.expects(:get)
|
|
||||||
.with("/test")
|
|
||||||
.raises(Faraday::TimeoutError)
|
|
||||||
.times(3)
|
|
||||||
|
|
||||||
response = @provider.fetch_data
|
|
||||||
|
|
||||||
assert_not response.success?
|
|
||||||
assert_match "timeout", response.error.message
|
|
||||||
end
|
|
||||||
|
|
||||||
test "fail, retry, succeed" do
|
|
||||||
client = mock
|
|
||||||
Faraday.stubs(:new).returns(client)
|
|
||||||
|
|
||||||
sequence = sequence("retry_sequence")
|
|
||||||
|
|
||||||
client.expects(:get)
|
|
||||||
.with("/test")
|
|
||||||
.raises(Faraday::TimeoutError)
|
|
||||||
.in_sequence(sequence)
|
|
||||||
|
|
||||||
client.expects(:get)
|
|
||||||
.with("/test")
|
|
||||||
.returns(Provider::Response.new(success?: true, data: "success", error: nil))
|
|
||||||
.in_sequence(sequence)
|
|
||||||
|
|
||||||
response = @provider.fetch_data
|
response = @provider.fetch_data
|
||||||
|
|
||||||
assert response.success?
|
assert response.success?
|
||||||
|
assert_equal({ some: "data" }, response.data)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns failed response with error" do
|
||||||
|
@client.expects(:get).with("/test").raises(StandardError.new("some error"))
|
||||||
|
|
||||||
|
response = @provider.fetch_data
|
||||||
|
|
||||||
|
assert_not response.success?
|
||||||
|
assert_equal("some error", response.error.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "provider can transform error" do
|
||||||
|
@client.expects(:get).with("/test").raises(StandardError.new("some error"))
|
||||||
|
|
||||||
|
response = @provider.fetch_data_with_error_transformer
|
||||||
|
|
||||||
|
assert_not response.success?
|
||||||
|
assert_equal("some error", response.error.message)
|
||||||
|
assert_instance_of TestProvider::TestError, response.error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue