package queue_test import ( "testing" "time" "github.com/lightningnetwork/lnd/queue" ) // testItem is an item type we'll be using to test the GCQueue. type testItem uint32 // TestGCQueueGCCycle asserts that items that are kept in the GCQueue past their // expiration will be released by a subsequent gc cycle. func TestGCQueueGCCycle(t *testing.T) { t.Parallel() const ( gcInterval = time.Second expiryInterval = 250 * time.Millisecond numItems = 6 ) newItem := func() interface{} { return new(testItem) } bp := queue.NewGCQueue(newItem, 100, gcInterval, expiryInterval) // Take numItems items from the queue, and immediately return them. // Returning the items will trigger the gc ticker to start. itemSet1 := takeN(t, bp, numItems) returnAll(bp, itemSet1) // Allow enough time for all expired items to be released by the queue. <-time.After(gcInterval + expiryInterval) // Take another set of numItems items from the queue. itemSet2 := takeN(t, bp, numItems) // Since the gc ticker should have elapsed, we expect the intersection // of sets 1 and 2 to be empty. for item := range itemSet2 { if _, ok := itemSet1[item]; ok { t.Fatalf("items taken should not have been reused") } } } // TestGCQueuePartialGCCycle asserts that the GCQueue will only garbage collect // the items in its queue that have fully expired. We test this by adding items // into the queue such that the garbage collection will occur before the items // expire. Taking items after the gc cycle should return the items that were not // released by the gc cycle. func TestGCQueuePartialGCCycle(t *testing.T) { t.Parallel() const ( gcInterval = time.Second expiryInterval = 250 * time.Millisecond numItems = 6 ) newItem := func() interface{} { return new(testItem) } bp := queue.NewGCQueue(newItem, 100, gcInterval, expiryInterval) // Take numItems items from the gc queue. itemSet1 := takeN(t, bp, numItems) // Immediately return half of the items, and construct a set of items // consisting of the half that were not returned. halfItemSet1 := returnN(t, bp, itemSet1, numItems/2) // Wait long enough to ensure that adding subsequent items will not be // released in the next gc cycle. <-time.After(gcInterval - expiryInterval/2) // Return the remaining items from itemSet1. returnAll(bp, halfItemSet1) // Wait until the gc cycle as done a sweep of the items and released all // those that have expired. <-time.After(expiryInterval / 2) // Retrieve numItems items from the gc queue. itemSet2 := takeN(t, bp, numItems) // Tally the number of items returned from Take that are in the second // half of items returned. var numReused int for item := range itemSet2 { if _, ok := halfItemSet1[item]; ok { numReused++ } } // We expect the number of reused items to be equal to half numItems. if numReused != numItems/2 { t.Fatalf("expected %d items to be reused, got %d", numItems/2, numReused) } } // takeN draws n items from the provided GCQueue. This method also asserts that // n unique items are drawn, and then returns the resulting set. func takeN(t *testing.T, q *queue.GCQueue, n int) map[interface{}]struct{} { t.Helper() items := make(map[interface{}]struct{}) for i := 0; i < n; i++ { // Wait a small duration to ensure the tests behave reliable, // and don't activate the non-blocking case unintentionally. <-time.After(time.Millisecond) items[q.Take()] = struct{}{} } if len(items) != n { t.Fatalf("items taken from gc queue should be distinct, "+ "want %d unique items, got %d", n, len(items)) } return items } // returnAll returns the items of the given set back to the GCQueue. func returnAll(q *queue.GCQueue, items map[interface{}]struct{}) { for item := range items { q.Return(item) // Wait a small duration to ensure the tests behave reliable, // and don't activate the non-blocking case unintentionally. <-time.After(time.Millisecond) } } // returnN returns n items at random from the set of items back to the GCQueue. // This method fails if the set's cardinality is smaller than n. func returnN(t *testing.T, q *queue.GCQueue, items map[interface{}]struct{}, n int) map[interface{}]struct{} { t.Helper() var remainingItems = make(map[interface{}]struct{}) var numReturned int for item := range items { if numReturned < n { q.Return(item) numReturned++ // Wait a small duration to ensure the tests behave // reliable, and don't activate the non-blocking case // unintentionally. <-time.After(time.Millisecond) } else { remainingItems[item] = struct{}{} } } if numReturned < n { t.Fatalf("insufficient number of items to return, need %d, "+ "got %d", n, numReturned) } return remainingItems }