multi: add buffer.Write and pool.WriteBuffer, make GCQueue generic

This commit is contained in:
Conner Fromknecht
2019-02-15 19:31:24 -08:00
parent ca4226d429
commit 6f96d04b72
12 changed files with 414 additions and 200 deletions

View File

@@ -8,21 +8,6 @@ import (
"github.com/lightningnetwork/lnd/ticker"
)
// Recycler is an interface that allows an object to be reclaimed without
// needing to be returned to the runtime.
type Recycler interface {
// Recycle resets the object to its default state.
Recycle()
}
// gcQueueEntry is a tuple containing a Recycler and the time at which the item
// was added to the queue. The recorded time is used to determine when the entry
// becomes stale, and can be released if it has not already been taken.
type gcQueueEntry struct {
item Recycler
time time.Time
}
// GCQueue is garbage collecting queue, which dynamically grows and contracts
// based on load. If the queue has items which have been returned, the queue
// will check every gcInterval amount of time to see if any elements are
@@ -36,15 +21,15 @@ type gcQueueEntry struct {
type GCQueue struct {
// takeBuffer coordinates the delivery of items taken from the queue
// such that they are delivered to requesters.
takeBuffer chan Recycler
takeBuffer chan interface{}
// returnBuffer coordinates the return of items back into the queue,
// where they will be kept until retaken or released.
returnBuffer chan Recycler
returnBuffer chan interface{}
// newItem is a constructor, used to generate new elements if none are
// otherwise available for reuse.
newItem func() Recycler
newItem func() interface{}
// expiryInterval is the minimum amount of time an element will remain
// in the queue before being released.
@@ -75,12 +60,12 @@ type GCQueue struct {
// the steady state. The returnQueueSize parameter is used to size the maximal
// number of items that can be returned without being dropped during large
// bursts in attempts to return items to the GCQUeue.
func NewGCQueue(newItem func() Recycler, returnQueueSize int,
func NewGCQueue(newItem func() interface{}, returnQueueSize int,
gcInterval, expiryInterval time.Duration) *GCQueue {
q := &GCQueue{
takeBuffer: make(chan Recycler),
returnBuffer: make(chan Recycler, returnQueueSize),
takeBuffer: make(chan interface{}),
returnBuffer: make(chan interface{}, returnQueueSize),
expiryInterval: expiryInterval,
freeList: list.New(),
recycleTicker: ticker.New(gcInterval),
@@ -95,7 +80,7 @@ func NewGCQueue(newItem func() Recycler, returnQueueSize int,
// Take returns either a recycled element from the queue, or creates a new item
// if none are available.
func (q *GCQueue) Take() Recycler {
func (q *GCQueue) Take() interface{} {
select {
case item := <-q.takeBuffer:
return item
@@ -107,20 +92,21 @@ func (q *GCQueue) Take() Recycler {
// Return adds the returned item to freelist if the queue's returnBuffer has
// available capacity. Under load, items may be dropped to ensure this method
// does not block.
func (q *GCQueue) Return(item Recycler) {
// Recycle the item to ensure that a dirty instance is never offered
// from Take. The call is done here so that the CPU cycles spent
// clearing the buffer are owned by the caller, and not by the queue
// itself. This makes the queue more likely to be available to deliver
// items in the free list.
item.Recycle()
func (q *GCQueue) Return(item interface{}) {
select {
case q.returnBuffer <- item:
default:
}
}
// gcQueueEntry is a tuple containing an interface{} and the time at which the
// item was added to the queue. The recorded time is used to determine when the
// entry becomes stale, and can be released if it has not already been taken.
type gcQueueEntry struct {
item interface{}
time time.Time
}
// queueManager maintains the free list of elements by popping the head of the
// queue when items are needed, and appending them to the end of the queue when
// items are returned. The queueManager will periodically attempt to release any
@@ -190,20 +176,20 @@ func (q *GCQueue) queueManager() {
next = e.Next()
entry := e.Value.(gcQueueEntry)
// Use now - insertTime > expiryInterval to
// determine if this entry has expired.
if time.Since(entry.time) > q.expiryInterval {
// Remove the expired entry from the
// linked-list.
q.freeList.Remove(e)
entry.item = nil
e.Value = nil
} else {
// Use now - insertTime <= expiryInterval to
// determine if this entry has not expired.
if time.Since(entry.time) <= q.expiryInterval {
// If this entry hasn't expired, then
// all entries that follow will still be
// valid.
break
}
// Otherwise, remove the expired entry from the
// linked-list.
q.freeList.Remove(e)
entry.item = nil
e.Value = nil
}
}
}

View File

@@ -7,10 +7,8 @@ import (
"github.com/lightningnetwork/lnd/queue"
)
// mockRecycler implements the queue.Recycler interface using a NOP.
type mockRecycler bool
func (*mockRecycler) Recycle() {}
// 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.
@@ -23,7 +21,7 @@ func TestGCQueueGCCycle(t *testing.T) {
numItems = 6
)
newItem := func() queue.Recycler { return new(mockRecycler) }
newItem := func() interface{} { return new(testItem) }
bp := queue.NewGCQueue(newItem, 100, gcInterval, expiryInterval)
@@ -61,7 +59,7 @@ func TestGCQueuePartialGCCycle(t *testing.T) {
numItems = 6
)
newItem := func() queue.Recycler { return new(mockRecycler) }
newItem := func() interface{} { return new(testItem) }
bp := queue.NewGCQueue(newItem, 100, gcInterval, expiryInterval)
@@ -104,10 +102,10 @@ func TestGCQueuePartialGCCycle(t *testing.T) {
// 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{} {
func takeN(t *testing.T, q *queue.GCQueue, n int) map[interface{}]struct{} {
t.Helper()
items := make(map[queue.Recycler]struct{})
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.
@@ -125,7 +123,7 @@ func takeN(t *testing.T, q *queue.GCQueue, n int) map[queue.Recycler]struct{} {
}
// returnAll returns the items of the given set back to the GCQueue.
func returnAll(q *queue.GCQueue, items map[queue.Recycler]struct{}) {
func returnAll(q *queue.GCQueue, items map[interface{}]struct{}) {
for item := range items {
q.Return(item)
@@ -138,11 +136,11 @@ func returnAll(q *queue.GCQueue, items map[queue.Recycler]struct{}) {
// 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{} {
items map[interface{}]struct{}, n int) map[interface{}]struct{} {
t.Helper()
var remainingItems = make(map[queue.Recycler]struct{})
var remainingItems = make(map[interface{}]struct{})
var numReturned int
for item := range items {
if numReturned < n {