diff --git a/queue/gc_queue_test.go b/queue/gc_queue_test.go new file mode 100644 index 000000000..cda0f998c --- /dev/null +++ b/queue/gc_queue_test.go @@ -0,0 +1,167 @@ +package queue_test + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/queue" +) + +// mockRecycler implements the queue.Recycler interface using a NOP. +type mockRecycler bool + +func (*mockRecycler) Recycle() {} + +// 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() queue.Recycler { return new(mockRecycler) } + + 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() queue.Recycler { return new(mockRecycler) } + + 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[queue.Recycler]struct{} { + t.Helper() + + items := make(map[queue.Recycler]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[queue.Recycler]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[queue.Recycler]struct{}, n int) map[queue.Recycler]struct{} { + + t.Helper() + + var remainingItems = make(map[queue.Recycler]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 +}