-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters