1
0
Fork 0
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:
Zach Gollwitzer 2025-04-01 08:41:49 -04:00 committed by GitHub
parent 0a17b84566
commit 939244bd3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 50 additions and 90 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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