mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
6b449a6633
and invoke the associated mission control migration.
306 lines
7.7 KiB
Go
306 lines
7.7 KiB
Go
package routing
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testMaxRecords = 2
|
|
|
|
var (
|
|
// mcStoreTestRoute is a test route for the mission control store tests.
|
|
mcStoreTestRoute = mcRoute{
|
|
totalAmount: lnwire.MilliSatoshi(5),
|
|
sourcePubKey: route.Vertex{1},
|
|
hops: []*mcHop{
|
|
{
|
|
pubKeyBytes: route.Vertex{2},
|
|
channelID: 4,
|
|
amtToFwd: lnwire.MilliSatoshi(7),
|
|
hasCustomRecords: true,
|
|
hasBlindingPoint: false,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// mcStoreTestHarness is the harness for a MissonControlStore test.
|
|
type mcStoreTestHarness struct {
|
|
db walletdb.DB
|
|
store *missionControlStore
|
|
}
|
|
|
|
// newMCStoreTestHarness initializes a test mission control store.
|
|
func newMCStoreTestHarness(t testing.TB, maxRecords int,
|
|
flushInterval time.Duration) mcStoreTestHarness {
|
|
// Set time zone explicitly to keep test deterministic.
|
|
time.Local = time.UTC
|
|
|
|
file, err := os.CreateTemp("", "*.db")
|
|
require.NoError(t, err)
|
|
|
|
dbPath := file.Name()
|
|
t.Cleanup(func() {
|
|
require.NoError(t, file.Close())
|
|
require.NoError(t, os.Remove(dbPath))
|
|
})
|
|
|
|
db, err := kvdb.Create(
|
|
kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, db.Close())
|
|
})
|
|
|
|
store, err := newMissionControlStore(
|
|
newDefaultNamespacedStore(db), maxRecords, flushInterval,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return mcStoreTestHarness{db: db, store: store}
|
|
}
|
|
|
|
// TestMissionControlStore tests the recording of payment failure events
|
|
// in mission control. It tests encoding and decoding of differing lnwire
|
|
// failures (FailIncorrectDetails and FailMppTimeout), pruning of results
|
|
// and idempotent writes.
|
|
func TestMissionControlStore(t *testing.T) {
|
|
h := newMCStoreTestHarness(t, testMaxRecords, time.Second)
|
|
db, store := h.db, h.store
|
|
|
|
results, err := store.fetchAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 0)
|
|
|
|
failureSourceIdx := 1
|
|
|
|
result1 := paymentResult{
|
|
route: &mcStoreTestRoute,
|
|
failure: lnwire.NewFailIncorrectDetails(100, 1000),
|
|
failureSourceIdx: &failureSourceIdx,
|
|
id: 99,
|
|
timeReply: testTime,
|
|
timeFwd: testTime.Add(-time.Minute),
|
|
}
|
|
|
|
result2 := result1
|
|
result2.timeReply = result1.timeReply.Add(time.Hour)
|
|
result2.timeFwd = result1.timeReply.Add(time.Hour)
|
|
result2.id = 2
|
|
|
|
// Store result.
|
|
store.AddResult(&result2)
|
|
|
|
// Store again to test idempotency.
|
|
store.AddResult(&result2)
|
|
|
|
// Store second result which has an earlier timestamp.
|
|
store.AddResult(&result1)
|
|
require.NoError(t, store.storeResults())
|
|
|
|
results, err = store.fetchAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 2)
|
|
|
|
// Check that results are stored in chronological order.
|
|
require.Equal(t, &result1, results[0])
|
|
require.Equal(t, &result2, results[1])
|
|
|
|
// Recreate store to test pruning.
|
|
store, err = newMissionControlStore(
|
|
newDefaultNamespacedStore(db), testMaxRecords, time.Second,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Add a newer result which failed due to mpp timeout.
|
|
result3 := result1
|
|
result3.timeReply = result1.timeReply.Add(2 * time.Hour)
|
|
result3.timeFwd = result1.timeReply.Add(2 * time.Hour)
|
|
result3.id = 3
|
|
result3.failure = &lnwire.FailMPPTimeout{}
|
|
|
|
store.AddResult(&result3)
|
|
require.NoError(t, store.storeResults())
|
|
|
|
// Check that results are pruned.
|
|
results, err = store.fetchAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 2)
|
|
|
|
require.Equal(t, &result2, results[0])
|
|
require.Equal(t, &result3, results[1])
|
|
}
|
|
|
|
// TestMissionControlStoreFlushing asserts the periodic flushing of the store
|
|
// works correctly.
|
|
func TestMissionControlStoreFlushing(t *testing.T) {
|
|
const flushInterval = 500 * time.Millisecond
|
|
|
|
h := newMCStoreTestHarness(t, testMaxRecords, flushInterval)
|
|
db, store := h.db, h.store
|
|
|
|
var (
|
|
failureSourceIdx = 1
|
|
failureDetails = lnwire.NewFailIncorrectDetails(100, 1000)
|
|
lastID uint64
|
|
)
|
|
nextResult := func() *paymentResult {
|
|
lastID += 1
|
|
return &paymentResult{
|
|
route: &mcStoreTestRoute,
|
|
failure: failureDetails,
|
|
failureSourceIdx: &failureSourceIdx,
|
|
id: lastID,
|
|
timeReply: testTime,
|
|
timeFwd: testTime.Add(-time.Minute),
|
|
}
|
|
}
|
|
|
|
// Helper to assert the number of results is correct.
|
|
assertResults := func(wantCount int) {
|
|
t.Helper()
|
|
err := wait.NoError(func() error {
|
|
results, err := store.fetchAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wantCount != len(results) {
|
|
return fmt.Errorf("wrong nb of results: got "+
|
|
"%d, want %d", len(results), wantCount)
|
|
}
|
|
if len(results) == 0 {
|
|
return nil
|
|
}
|
|
gotLastID := results[len(results)-1].id
|
|
if len(results) > 0 && gotLastID != lastID {
|
|
return fmt.Errorf("wrong id for last item: "+
|
|
"got %d, want %d", gotLastID, lastID)
|
|
}
|
|
|
|
return nil
|
|
}, flushInterval*5)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Run the store.
|
|
store.run()
|
|
time.Sleep(flushInterval)
|
|
|
|
// Wait for the flush interval. There should be no records.
|
|
assertResults(0)
|
|
|
|
// Store a result and check immediately. There still shouldn't be
|
|
// any results stored (flush interval has not elapsed).
|
|
store.AddResult(nextResult())
|
|
assertResults(0)
|
|
|
|
// Assert that eventually the result is stored after being flushed.
|
|
assertResults(1)
|
|
|
|
// Store enough results that fill the max number of results.
|
|
for i := 0; i < testMaxRecords; i++ {
|
|
store.AddResult(nextResult())
|
|
}
|
|
assertResults(testMaxRecords)
|
|
|
|
// Finally, stop the store to recreate it.
|
|
store.stop()
|
|
|
|
// Recreate store.
|
|
store, err := newMissionControlStore(
|
|
newDefaultNamespacedStore(db), testMaxRecords, flushInterval,
|
|
)
|
|
require.NoError(t, err)
|
|
store.run()
|
|
defer store.stop()
|
|
time.Sleep(flushInterval)
|
|
assertResults(testMaxRecords)
|
|
|
|
// Fill the store with results again.
|
|
for i := 0; i < testMaxRecords; i++ {
|
|
store.AddResult(nextResult())
|
|
}
|
|
assertResults(testMaxRecords)
|
|
}
|
|
|
|
// BenchmarkMissionControlStoreFlushing benchmarks the periodic storage of data
|
|
// from the mission control store when additional results are added between
|
|
// runs.
|
|
func BenchmarkMissionControlStoreFlushing(b *testing.B) {
|
|
var (
|
|
failureSourceIdx = 1
|
|
failureDetails = lnwire.NewFailIncorrectDetails(100, 1000)
|
|
testTimeFwd = testTime.Add(-time.Minute)
|
|
|
|
tests = []int{0, 1, 10, 100, 250, 500}
|
|
)
|
|
|
|
const testMaxRecords = 1000
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
name := fmt.Sprintf("%v additional results", tc)
|
|
b.Run(name, func(b *testing.B) {
|
|
h := newMCStoreTestHarness(
|
|
b, testMaxRecords, time.Second,
|
|
)
|
|
store := h.store
|
|
|
|
// Fill the store.
|
|
var lastID uint64
|
|
for i := 0; i < testMaxRecords; i++ {
|
|
lastID++
|
|
result := &paymentResult{
|
|
route: &mcStoreTestRoute,
|
|
failure: failureDetails,
|
|
failureSourceIdx: &failureSourceIdx,
|
|
id: lastID,
|
|
timeReply: testTime,
|
|
timeFwd: testTimeFwd,
|
|
}
|
|
store.AddResult(result)
|
|
}
|
|
|
|
// Do the first flush.
|
|
err := store.storeResults()
|
|
require.NoError(b, err)
|
|
|
|
// Create the additional results.
|
|
results := make([]*paymentResult, tc)
|
|
for i := 0; i < len(results); i++ {
|
|
results[i] = &paymentResult{
|
|
route: &mcStoreTestRoute,
|
|
failure: failureDetails,
|
|
failureSourceIdx: &failureSourceIdx,
|
|
timeReply: testTime,
|
|
timeFwd: testTimeFwd,
|
|
}
|
|
}
|
|
|
|
// Run the actual benchmark.
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
for j := 0; j < len(results); j++ {
|
|
lastID++
|
|
results[j].id = lastID
|
|
store.AddResult(results[j])
|
|
}
|
|
err := store.storeResults()
|
|
require.NoError(b, err)
|
|
}
|
|
})
|
|
}
|
|
}
|