github.com/decred/dcrlnd@v0.7.6/routing/missioncontrol_store_test.go (about)

     1  package routing
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"reflect"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/davecgh/go-spew/spew"
    12  	"github.com/decred/dcrlnd/lntest/wait"
    13  	"github.com/decred/dcrlnd/lnwire"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/decred/dcrlnd/kvdb"
    17  	"github.com/decred/dcrlnd/routing/route"
    18  )
    19  
    20  const testMaxRecords = 2
    21  
    22  // TestMissionControlStore tests the recording of payment failure events
    23  // in mission control. It tests encoding and decoding of differing lnwire
    24  // failures (FailIncorrectDetails and FailMppTimeout), pruning of results
    25  // and idempotent writes.
    26  func TestMissionControlStore(t *testing.T) {
    27  	// Set time zone explicitly to keep test deterministic.
    28  	time.Local = time.UTC
    29  
    30  	file, err := ioutil.TempFile("", "*.db")
    31  	if err != nil {
    32  		t.Fatal(err)
    33  	}
    34  
    35  	dbPath := file.Name()
    36  
    37  	db, err := kvdb.Create(
    38  		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
    39  	)
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	}
    43  	defer db.Close()
    44  	defer os.Remove(dbPath)
    45  
    46  	store, err := newMissionControlStore(db, testMaxRecords, time.Second)
    47  	if err != nil {
    48  		t.Fatal(err)
    49  	}
    50  
    51  	results, err := store.fetchAll()
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	if len(results) != 0 {
    56  		t.Fatal("expected no results")
    57  	}
    58  
    59  	testRoute := route.Route{
    60  		SourcePubKey: route.Vertex{1},
    61  		Hops: []*route.Hop{
    62  			{
    63  				PubKeyBytes:   route.Vertex{2},
    64  				LegacyPayload: true,
    65  			},
    66  		},
    67  	}
    68  
    69  	failureSourceIdx := 1
    70  
    71  	result1 := paymentResult{
    72  		route:            &testRoute,
    73  		failure:          lnwire.NewFailIncorrectDetails(100, 1000),
    74  		failureSourceIdx: &failureSourceIdx,
    75  		id:               99,
    76  		timeReply:        testTime,
    77  		timeFwd:          testTime.Add(-time.Minute),
    78  	}
    79  
    80  	result2 := result1
    81  	result2.timeReply = result1.timeReply.Add(time.Hour)
    82  	result2.timeFwd = result1.timeReply.Add(time.Hour)
    83  	result2.id = 2
    84  
    85  	// Store result.
    86  	store.AddResult(&result2)
    87  
    88  	// Store again to test idempotency.
    89  	store.AddResult(&result2)
    90  
    91  	// Store second result which has an earlier timestamp.
    92  	store.AddResult(&result1)
    93  	require.NoError(t, store.storeResults())
    94  
    95  	results, err = store.fetchAll()
    96  	if err != nil {
    97  		t.Fatal(err)
    98  	}
    99  	require.Equal(t, 2, len(results))
   100  
   101  	if len(results) != 2 {
   102  		t.Fatal("expected two results")
   103  	}
   104  
   105  	// Check that results are stored in chronological order.
   106  	if !reflect.DeepEqual(&result1, results[0]) {
   107  		t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result1),
   108  			spew.Sdump(results[0]))
   109  	}
   110  	if !reflect.DeepEqual(&result2, results[1]) {
   111  		t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result2),
   112  			spew.Sdump(results[1]))
   113  	}
   114  
   115  	// Recreate store to test pruning.
   116  	store, err = newMissionControlStore(db, testMaxRecords, time.Second)
   117  	if err != nil {
   118  		t.Fatal(err)
   119  	}
   120  
   121  	// Add a newer result which failed due to mpp timeout.
   122  	result3 := result1
   123  	result3.timeReply = result1.timeReply.Add(2 * time.Hour)
   124  	result3.timeFwd = result1.timeReply.Add(2 * time.Hour)
   125  	result3.id = 3
   126  	result3.failure = &lnwire.FailMPPTimeout{}
   127  
   128  	store.AddResult(&result3)
   129  	require.NoError(t, store.storeResults())
   130  
   131  	// Check that results are pruned.
   132  	results, err = store.fetchAll()
   133  	if err != nil {
   134  		t.Fatal(err)
   135  	}
   136  	require.Equal(t, 2, len(results))
   137  	if len(results) != 2 {
   138  		t.Fatal("expected two results")
   139  	}
   140  
   141  	if !reflect.DeepEqual(&result2, results[0]) {
   142  		t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result2),
   143  			spew.Sdump(results[0]))
   144  	}
   145  	if !reflect.DeepEqual(&result3, results[1]) {
   146  		t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result3),
   147  			spew.Sdump(results[1]))
   148  	}
   149  }
   150  
   151  // TestMissionControlStoreFlushing asserts the periodic flushing of the store
   152  // works correctly.
   153  func TestMissionControlStoreFlushing(t *testing.T) {
   154  	// Set time zone explicitly to keep test deterministic.
   155  	time.Local = time.UTC
   156  
   157  	file, err := os.CreateTemp("", "*.db")
   158  	require.NoError(t, err)
   159  
   160  	dbPath := file.Name()
   161  	t.Cleanup(func() {
   162  		require.NoError(t, file.Close())
   163  		require.NoError(t, os.Remove(dbPath))
   164  	})
   165  
   166  	db, err := kvdb.Create(
   167  		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
   168  	)
   169  	require.NoError(t, err)
   170  	t.Cleanup(func() {
   171  		require.NoError(t, db.Close())
   172  	})
   173  
   174  	const flushInterval = time.Second
   175  
   176  	store, err := newMissionControlStore(db, testMaxRecords, flushInterval)
   177  	require.NoError(t, err)
   178  
   179  	testRoute := route.Route{
   180  		SourcePubKey: route.Vertex{1},
   181  		Hops: []*route.Hop{
   182  			{
   183  				PubKeyBytes:   route.Vertex{2},
   184  				LegacyPayload: true,
   185  			},
   186  		},
   187  	}
   188  
   189  	failureSourceIdx := 1
   190  	failureDetails := lnwire.NewFailIncorrectDetails(100, 1000)
   191  	var lastID uint64 = 0
   192  	nextResult := func() *paymentResult {
   193  		lastID += 1
   194  		return &paymentResult{
   195  			route:            &testRoute,
   196  			failure:          failureDetails,
   197  			failureSourceIdx: &failureSourceIdx,
   198  			id:               lastID,
   199  			timeReply:        testTime,
   200  			timeFwd:          testTime.Add(-time.Minute),
   201  		}
   202  	}
   203  
   204  	// Helper to assert the number of results is correct.
   205  	assertResults := func(wantCount int) {
   206  		t.Helper()
   207  		err := wait.NoError(func() error {
   208  			results, err := store.fetchAll()
   209  			if err != nil {
   210  				return err
   211  			}
   212  			if wantCount != len(results) {
   213  				return fmt.Errorf("wrong nb of results: got "+
   214  					"%d, want %d", len(results), wantCount)
   215  			}
   216  			if len(results) == 0 {
   217  				return nil
   218  			}
   219  			gotLastID := results[len(results)-1].id
   220  			if len(results) > 0 && gotLastID != lastID {
   221  				return fmt.Errorf("wrong id for last item: "+
   222  					"got %d, want %d", gotLastID, lastID)
   223  			}
   224  
   225  			return nil
   226  		}, flushInterval*5)
   227  		require.NoError(t, err)
   228  	}
   229  
   230  	// Run the store.
   231  	store.run()
   232  	time.Sleep(flushInterval)
   233  
   234  	// Wait for the flush interval. There should be no records.
   235  	assertResults(0)
   236  
   237  	// Store a result and check immediately. There still shouldn't be
   238  	// any results stored (flush interval has not elapsed).
   239  	store.AddResult(nextResult())
   240  	assertResults(0)
   241  
   242  	// Assert that eventually the result is stored after being flushed.
   243  	assertResults(1)
   244  
   245  	// Store enough results that fill the max nb of results.
   246  	for i := 0; i < testMaxRecords+1; i++ {
   247  		store.AddResult(nextResult())
   248  	}
   249  	assertResults(testMaxRecords)
   250  
   251  	// Finally, stop the store to recreate it.
   252  	store.stop()
   253  
   254  	// Recreate store.
   255  	store, err = newMissionControlStore(db, testMaxRecords, flushInterval)
   256  	require.NoError(t, err)
   257  	store.run()
   258  	defer store.stop()
   259  	time.Sleep(flushInterval)
   260  	assertResults(testMaxRecords)
   261  
   262  	// Fill the store with results again.
   263  	for i := 0; i < testMaxRecords+1; i++ {
   264  		store.AddResult(nextResult())
   265  	}
   266  	assertResults(testMaxRecords)
   267  }
   268  
   269  // drainMCStoreQueueChan drains the store's queueChan when the test does not
   270  // issue a run().
   271  func drainMCStoreQueueChan(t testing.TB, store *missionControlStore) {
   272  	done := make(chan struct{})
   273  	t.Cleanup(func() { close(done) })
   274  	go func() {
   275  		for {
   276  			select {
   277  			case <-done:
   278  			case <-store.queueChan:
   279  			}
   280  		}
   281  	}()
   282  }
   283  
   284  // BenchmarkMissionControlStoreFlushingNoWork benchmarks the periodic storage
   285  // of data from the mission control store when no additional results are added
   286  // between runs.
   287  func BenchmarkMissionControlStoreFlushing(b *testing.B) {
   288  	testRoute := route.Route{
   289  		SourcePubKey: route.Vertex{1},
   290  		Hops: []*route.Hop{
   291  			{
   292  				PubKeyBytes:   route.Vertex{2},
   293  				LegacyPayload: true,
   294  			},
   295  		},
   296  	}
   297  
   298  	failureSourceIdx := 1
   299  	failureDetails := lnwire.NewFailIncorrectDetails(100, 1000)
   300  	testTimeFwd := testTime.Add(-time.Minute)
   301  
   302  	const testMaxRecords = 1000
   303  
   304  	tests := []struct {
   305  		name      string
   306  		nbResults int
   307  		doBench   func(b *testing.B, store *missionControlStore)
   308  	}{{
   309  		name:      "no additional results",
   310  		nbResults: 0,
   311  	}, {
   312  		name:      "one additional result",
   313  		nbResults: 1,
   314  	}, {
   315  		name:      "ten additional results",
   316  		nbResults: 10,
   317  	}, {
   318  		name:      "100 additional results",
   319  		nbResults: 100,
   320  	}, {
   321  		name:      "250 additional results",
   322  		nbResults: 250,
   323  	}, {
   324  		name:      "500 additional results",
   325  		nbResults: 500,
   326  	}}
   327  
   328  	for _, tc := range tests {
   329  		tc := tc
   330  		b.Run(tc.name, func(b *testing.B) {
   331  			// Set time zone explicitly to keep test deterministic.
   332  			time.Local = time.UTC
   333  
   334  			file, err := os.CreateTemp("", "*.db")
   335  			require.NoError(b, err)
   336  
   337  			dbPath := file.Name()
   338  			b.Cleanup(func() {
   339  				require.NoError(b, file.Close())
   340  				require.NoError(b, os.Remove(dbPath))
   341  			})
   342  
   343  			db, err := kvdb.Create(
   344  				kvdb.BoltBackendName, dbPath, true,
   345  				kvdb.DefaultDBTimeout,
   346  			)
   347  			require.NoError(b, err)
   348  			b.Cleanup(func() {
   349  				require.NoError(b, db.Close())
   350  			})
   351  
   352  			store, err := newMissionControlStore(
   353  				db, testMaxRecords, time.Second,
   354  			)
   355  			require.NoError(b, err)
   356  
   357  			// Fill the store.
   358  			var lastID uint64
   359  			for i := 0; i < testMaxRecords; i++ {
   360  				lastID++
   361  				result := &paymentResult{
   362  					route:            &testRoute,
   363  					failure:          failureDetails,
   364  					failureSourceIdx: &failureSourceIdx,
   365  					id:               lastID,
   366  					timeReply:        testTime,
   367  					timeFwd:          testTimeFwd,
   368  				}
   369  				store.AddResult(result)
   370  			}
   371  
   372  			// Do the first flush.
   373  			err = store.storeResults()
   374  			require.NoError(b, err)
   375  			<-store.queueChan
   376  
   377  			// Create the additional results.
   378  			results := make([]*paymentResult, tc.nbResults)
   379  			for i := 0; i < len(results); i++ {
   380  				results[i] = &paymentResult{
   381  					route:            &testRoute,
   382  					failure:          failureDetails,
   383  					failureSourceIdx: &failureSourceIdx,
   384  					timeReply:        testTime,
   385  					timeFwd:          testTimeFwd,
   386  				}
   387  			}
   388  
   389  			// Run the actual benchmark.
   390  			b.ResetTimer()
   391  			b.ReportAllocs()
   392  
   393  			for i := 0; i < b.N; i++ {
   394  				for j := 0; j < len(results); j++ {
   395  					lastID++
   396  					results[j].id = lastID
   397  					store.AddResult(results[j])
   398  				}
   399  				if len(results) > 0 {
   400  					<-store.queueChan
   401  				}
   402  				err := store.storeResults()
   403  				require.NoError(b, err)
   404  			}
   405  		})
   406  	}
   407  }