Skip to content

Commit

Permalink
fiat: add fiat package with multiple retry method
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
carlaKC committed Apr 30, 2020
1 parent 044230b commit 1b923cb
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 0 deletions.
54 changes: 54 additions & 0 deletions fiat/fiat.go
Original file line number Diff line number Diff line change
@@ -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 changes: 121 additions & 0 deletions fiat/fiat_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions fiat/log.go
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down

0 comments on commit 1b923cb

Please sign in to comment.