lnwallet: add configurable cache for web fee estimator

Add fee.min-update-timeout and fee.max-update-timeout config options to
allow configuration of the web fee estimator cache.
This commit is contained in:
Tom Kirkpatrick authored and yyforyongyu committed 2024-05-04 14:41:41 +08:00
1 parent fa616ee059
commit 3837c3f12e
15 files changed
+202 -55

No files matched your search

+41 -18
View File
@@ -28,14 +28,6 @@ const (
// less than this will result in an error.
minBlockTarget uint32 = 1
// minFeeUpdateTimeout represents the minimum interval in which a
// WebAPIEstimator will request fresh fees from its API.
minFeeUpdateTimeout = 5 * time.Minute
// maxFeeUpdateTimeout represents the maximum interval in which a
// WebAPIEstimator will request fresh fees from its API.
maxFeeUpdateTimeout = 20 * time.Minute
// WebAPIConnectionTimeout specifies the timeout value for connecting
// to the api source.
WebAPIConnectionTimeout = 5 * time.Second
@@ -739,19 +731,43 @@ type WebAPIEstimator struct {
// estimates.
noCache bool
// minFeeUpdateTimeout represents the minimum interval in which the
// web estimator will request fresh fees from its API.
minFeeUpdateTimeout time.Duration
// minFeeUpdateTimeout represents the maximum interval in which the
// web estimator will request fresh fees from its API.
maxFeeUpdateTimeout time.Duration
quit chan struct{}
wg sync.WaitGroup
}
// NewWebAPIEstimator creates a new WebAPIEstimator from a given URL and a
// fallback default fee. The fees are updated whenever a new block is mined.
func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool) *WebAPIEstimator {
return &WebAPIEstimator{
apiSource: api,
feeByBlockTarget: make(map[uint32]uint32),
noCache: noCache,
quit: make(chan struct{}),
func NewWebAPIEstimator(api WebAPIFeeSource, noCache bool,
minFeeUpdateTimeout time.Duration,
maxFeeUpdateTimeout time.Duration) (*WebAPIEstimator, error) {
if minFeeUpdateTimeout == 0 || maxFeeUpdateTimeout == 0 {
return nil, fmt.Errorf("minFeeUpdateTimeout and " +
"maxFeeUpdateTimeout must be greater than 0")
}
if minFeeUpdateTimeout >= maxFeeUpdateTimeout {
return nil, fmt.Errorf("minFeeUpdateTimeout target of %v "+
"cannot be greater than maxFeeUpdateTimeout of %v",
minFeeUpdateTimeout, maxFeeUpdateTimeout)
}
return &WebAPIEstimator{
apiSource: api,
feeByBlockTarget: make(map[uint32]uint32),
noCache: noCache,
quit: make(chan struct{}),
minFeeUpdateTimeout: minFeeUpdateTimeout,
maxFeeUpdateTimeout: maxFeeUpdateTimeout,
}, nil
}
// EstimateFeePerKW takes in a target for the number of blocks until an initial
@@ -809,7 +825,12 @@ func (w *WebAPIEstimator) Start() error {
w.started.Do(func() {
log.Infof("Starting web API fee estimator")
w.updateFeeTicker = time.NewTicker(w.randomFeeUpdateTimeout())
feeUpdateTimeout := w.randomFeeUpdateTimeout()
log.Infof("Web API fee estimator using update timeout of %v",
feeUpdateTimeout)
w.updateFeeTicker = time.NewTicker(feeUpdateTimeout)
w.updateFeeEstimates()
w.wg.Add(1)
@@ -852,9 +873,11 @@ func (w *WebAPIEstimator) RelayFeePerKW() SatPerKWeight {
// and maxFeeUpdateTimeout that will be used to determine how often the Estimator
// should retrieve fresh fees from its API.
func (w *WebAPIEstimator) randomFeeUpdateTimeout() time.Duration {
lower := int64(minFeeUpdateTimeout)
upper := int64(maxFeeUpdateTimeout)
return time.Duration(prand.Int63n(upper-lower) + lower)
lower := int64(w.minFeeUpdateTimeout)
upper := int64(w.maxFeeUpdateTimeout)
return time.Duration(
prand.Int63n(upper-lower) + lower, //nolint:gosec
).Round(time.Second)
}
// getCachedFee takes a conf target and returns the cached fee rate. When the
+48 -2
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/stretchr/testify/require"
@@ -142,6 +143,9 @@ func TestWebAPIFeeEstimator(t *testing.T) {
// Fee rates are in sat/kb.
minFeeRate uint32 = 2000 // 500 sat/kw
maxFeeRate uint32 = 4000 // 1000 sat/kw
minFeeUpdateTimeout = 5 * time.Minute
maxFeeUpdateTimeout = 20 * time.Minute
)
testCases := []struct {
@@ -199,7 +203,9 @@ func TestWebAPIFeeEstimator(t *testing.T) {
feeSource := &mockFeeSource{}
feeSource.On("GetFeeMap").Return(feeRateResp, nil)
estimator := NewWebAPIEstimator(feeSource, false)
estimator, _ := NewWebAPIEstimator(
feeSource, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
)
// Test that requesting a fee when no fees have been cached won't fail.
feeRate, err := estimator.EstimateFeePerKW(5)
@@ -247,10 +253,15 @@ func TestGetCachedFee(t *testing.T) {
minFeeRate uint32 = 100
maxFeeRate uint32 = 1000
minFeeUpdateTimeout = 5 * time.Minute
maxFeeUpdateTimeout = 20 * time.Minute
)
// Create a dummy estimator without WebAPIFeeSource.
estimator := NewWebAPIEstimator(nil, false)
estimator, _ := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
)
// When the cache is empty, an error should be returned.
cachedFee, err := estimator.getCachedFee(minTarget)
@@ -315,3 +326,38 @@ func TestGetCachedFee(t *testing.T) {
})
}
}
func TestRandomFeeUpdateTimeout(t *testing.T) {
t.Parallel()
var (
minFeeUpdateTimeout = 1 * time.Minute
maxFeeUpdateTimeout = 2 * time.Minute
)
estimator, _ := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
)
for i := 0; i < 1000; i++ {
timeout := estimator.randomFeeUpdateTimeout()
require.GreaterOrEqual(t, timeout, minFeeUpdateTimeout)
require.LessOrEqual(t, timeout, maxFeeUpdateTimeout)
}
}
func TestInvalidFeeUpdateTimeout(t *testing.T) {
t.Parallel()
var (
minFeeUpdateTimeout = 2 * time.Minute
maxFeeUpdateTimeout = 1 * time.Minute
)
_, err := NewWebAPIEstimator(
nil, false, minFeeUpdateTimeout, maxFeeUpdateTimeout,
)
require.Error(t, err, "NewWebAPIEstimator should return an error "+
"when minFeeUpdateTimeout > maxFeeUpdateTimeout")
}