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 }