mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
fiat: add fiat package with multiple retry method
Add a retry query method which can be used to repetitively query an external api, with sleeps in between failed queries. We provide a quit channel which can be used to terminate this process early if required.
This commit is contained in:
parent
044230bb06
commit
1b923cbb95
4 changed files with 203 additions and 0 deletions
54
fiat/fiat.go
Normal file
54
fiat/fiat.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package fiat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxRetries is the maximum number of retries we allow per call to an
|
||||
// api.
|
||||
maxRetries = 3
|
||||
|
||||
// retrySleep is the period we backoff for between tries, set to 0.5
|
||||
// second.
|
||||
retrySleep = time.Millisecond * 500
|
||||
)
|
||||
|
||||
var (
|
||||
errShuttingDown = errors.New("shutting down")
|
||||
errRetriesFailed = errors.New("could not get data within max retries")
|
||||
)
|
||||
|
||||
// usdPrice represents the Bitcoin price in USD at a certain time.
|
||||
type usdPrice struct{}
|
||||
|
||||
// retryQuery calls an api until it succeeds, or we hit our maximum retries.
|
||||
// It sleeps between calls and can be terminated early by cancelling the
|
||||
// context passed in. It takes query and convert functions as parameters for
|
||||
// testing purposes.
|
||||
func retryQuery(ctx context.Context, queryAPI func() ([]byte, error),
|
||||
convert func([]byte) ([]*usdPrice, error)) ([]*usdPrice, error) {
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
// If our request fails, log the error, sleep for the retry
|
||||
// period and then continue so we can try again.
|
||||
response, err := queryAPI()
|
||||
if err != nil {
|
||||
log.Errorf("http get attempt: %v failed: %v", i, err)
|
||||
|
||||
select {
|
||||
case <-time.After(retrySleep):
|
||||
case <-ctx.Done():
|
||||
return nil, errShuttingDown
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return convert(response)
|
||||
}
|
||||
|
||||
return nil, errRetriesFailed
|
||||
}
|
||||
121
fiat/fiat_test.go
Normal file
121
fiat/fiat_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package fiat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var errMocked = errors.New("mocked error")
|
||||
|
||||
// fakeQuery mocks failing and successful repeated queries, failing after a
|
||||
// given call count.
|
||||
type fakeQuery struct {
|
||||
// callCount tracks the number of times the httpQuery has been executed.
|
||||
callCount int
|
||||
|
||||
// errorUntil is the call count at which the mock will stop failing.
|
||||
// If you do want the mock call to succeed, set this value to -1.
|
||||
// Eg, if this value is set to 2, the call will fail on first call and
|
||||
// succeed thereafter.
|
||||
errorUntil int
|
||||
}
|
||||
|
||||
func (f *fakeQuery) call() error {
|
||||
f.callCount++
|
||||
|
||||
if f.callCount <= f.errorUntil {
|
||||
return errMocked
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestRetryQuery tests our retry logic, including the case where we receive
|
||||
// instruction to shutdown.
|
||||
func TestRetryQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedCallCount int
|
||||
expectedErr error
|
||||
cancelContext bool
|
||||
mock *fakeQuery
|
||||
}{
|
||||
{
|
||||
name: "always failing",
|
||||
expectedCallCount: maxRetries,
|
||||
expectedErr: errRetriesFailed,
|
||||
mock: &fakeQuery{
|
||||
errorUntil: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "last call succeeds",
|
||||
expectedCallCount: 3,
|
||||
expectedErr: nil,
|
||||
mock: &fakeQuery{
|
||||
errorUntil: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "first succeeds",
|
||||
expectedCallCount: 1,
|
||||
expectedErr: nil,
|
||||
mock: &fakeQuery{
|
||||
errorUntil: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call cancelled",
|
||||
expectedCallCount: 1,
|
||||
expectedErr: errShuttingDown,
|
||||
cancelContext: true,
|
||||
mock: &fakeQuery{
|
||||
errorUntil: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test context which we can cancel.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// If we want to cancel test context to test early exit,
|
||||
// go do now.
|
||||
if test.cancelContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
query := func() ([]byte, error) {
|
||||
if err := test.mock.call(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a mocked parse call which acts as a nop.
|
||||
parse := func([]byte) ([]*usdPrice, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err := retryQuery(ctx, query, parse)
|
||||
if err != test.expectedErr {
|
||||
t.Fatalf("expected: %v, got: %v",
|
||||
test.expectedErr, err)
|
||||
}
|
||||
|
||||
if test.mock.callCount != test.expectedCallCount {
|
||||
t.Fatalf("expected call count: %v, got :%v",
|
||||
test.expectedCallCount,
|
||||
test.mock.callCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
26
fiat/log.go
Normal file
26
fiat/log.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package fiat
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// Subsystem defines the logging code for this subsystem.
|
||||
const Subsystem = "FIAT"
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the
|
||||
// caller requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
2
log.go
2
log.go
|
|
@ -3,6 +3,7 @@ package faraday
|
|||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightninglabs/faraday/dataset"
|
||||
"github.com/lightninglabs/faraday/fiat"
|
||||
"github.com/lightninglabs/faraday/frdrpc"
|
||||
"github.com/lightninglabs/faraday/recommend"
|
||||
"github.com/lightninglabs/faraday/revenue"
|
||||
|
|
@ -28,6 +29,7 @@ func init() {
|
|||
addSubLogger(dataset.Subsystem, dataset.UseLogger)
|
||||
addSubLogger(frdrpc.Subsystem, frdrpc.UseLogger)
|
||||
addSubLogger(revenue.Subsystem, revenue.UseLogger)
|
||||
addSubLogger(fiat.Subsystem, fiat.UseLogger)
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue