lnd/routing/missioncontrol_store_test.go
Elle Mouton 2dd9046622
routing: introduce missionControlDB abstraction
So that `missionControlStore` can be unaware of the backing DB structure
it is writing to. In an upcoming commit when we change mission control
to write to namespaced buckets instead, we then only need to update the
`namespacedDB` implementation.
2024-10-01 13:50:50 +02:00

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(
newNamespacedDB(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(
newNamespacedDB(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(
newNamespacedDB(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)
}
})
}
}