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:
carla 2020-04-28 20:59:07 +02:00
parent 044230bb06
commit 1b923cbb95
No known key found for this signature in database
GPG key ID: 4CA7FE54A6213C91
4 changed files with 203 additions and 0 deletions

54
fiat/fiat.go Normal file
View 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
View 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
View 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
View file

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