lnd/routing/missioncontrol_store_test.go
Matheus Degiovani 6a27bc29ba
missioncontrolstore: add additional tests and benchmarks
These will be useful in the next commits.
2024-06-19 07:33:14 -03:00

296 lines
7.5 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 = route.Route{
SourcePubKey: route.Vertex{1},
Hops: []*route.Hop{
{
PubKeyBytes: route.Vertex{2},
LegacyPayload: true,
},
},
}
)
// 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(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(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(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)
}
})
}
}