211 lines
7.3 KiB
Go
211 lines
7.3 KiB
Go
|
package queue
|
||
|
|
||
|
import (
|
||
|
"container/list"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"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
|
||
|
// eligible to be released back to the runtime. Elements that have been in the
|
||
|
// queue for a duration of least expiryInterval will be released upon the next
|
||
|
// iteration of the garbage collection, thus the maximum amount of time an
|
||
|
// element remain in the queue is expiryInterval+gcInterval. The gc ticker will
|
||
|
// be disabled after all items in the queue have been taken or released to
|
||
|
// ensure that the GCQueue becomes quiescent, and imposes minimal overhead in
|
||
|
// the steady state.
|
||
|
type GCQueue struct {
|
||
|
// takeBuffer coordinates the delivery of items taken from the queue
|
||
|
// such that they are delivered to requesters.
|
||
|
takeBuffer chan Recycler
|
||
|
|
||
|
// returnBuffer coordinates the return of items back into the queue,
|
||
|
// where they will be kept until retaken or released.
|
||
|
returnBuffer chan Recycler
|
||
|
|
||
|
// newItem is a constructor, used to generate new elements if none are
|
||
|
// otherwise available for reuse.
|
||
|
newItem func() Recycler
|
||
|
|
||
|
// expiryInterval is the minimum amount of time an element will remain
|
||
|
// in the queue before being released.
|
||
|
expiryInterval time.Duration
|
||
|
|
||
|
// recycleTicker is a resumable ticker used to trigger a sweep to
|
||
|
// release elements that have been in the queue longer than
|
||
|
// expiryInterval.
|
||
|
recycleTicker ticker.Ticker
|
||
|
|
||
|
// freeList maintains a list of gcQueueEntries, sorted in order of
|
||
|
// increasing time of arrival.
|
||
|
freeList *list.List
|
||
|
|
||
|
wg sync.WaitGroup
|
||
|
quit chan struct{}
|
||
|
}
|
||
|
|
||
|
// NewGCQueue creates a new 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 eligible to be released back to the runtime. Elements that have been in
|
||
|
// the queue for a duration of least expiryInterval will be released upon the
|
||
|
// next iteration of the garbage collection, thus the maximum amount of time an
|
||
|
// element remain in the queue is expiryInterval+gcInterval. The gc ticker will
|
||
|
// be disabled after all items in the queue have been taken or released to
|
||
|
// ensure that the GCQueue becomes quiescent, and imposes minimal overhead in
|
||
|
// 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,
|
||
|
gcInterval, expiryInterval time.Duration) *GCQueue {
|
||
|
|
||
|
q := &GCQueue{
|
||
|
takeBuffer: make(chan Recycler),
|
||
|
returnBuffer: make(chan Recycler, returnQueueSize),
|
||
|
expiryInterval: expiryInterval,
|
||
|
freeList: list.New(),
|
||
|
recycleTicker: ticker.New(gcInterval),
|
||
|
newItem: newItem,
|
||
|
quit: make(chan struct{}),
|
||
|
}
|
||
|
|
||
|
go q.queueManager()
|
||
|
|
||
|
return q
|
||
|
}
|
||
|
|
||
|
// Take returns either a recycled element from the queue, or creates a new item
|
||
|
// if none are available.
|
||
|
func (q *GCQueue) Take() Recycler {
|
||
|
select {
|
||
|
case item := <-q.takeBuffer:
|
||
|
return item
|
||
|
case <-time.After(time.Millisecond):
|
||
|
return q.newItem()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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()
|
||
|
|
||
|
select {
|
||
|
case q.returnBuffer <- item:
|
||
|
default:
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
// items that have been in the queue longer than the expiry interval.
|
||
|
//
|
||
|
// NOTE: This method SHOULD be run as a goroutine.
|
||
|
func (q *GCQueue) queueManager() {
|
||
|
for {
|
||
|
// If the pool is empty, initialize a buffer pool to serve a
|
||
|
// client that takes a buffer immediately. If this happens, this
|
||
|
// is either:
|
||
|
// 1) the first iteration of the loop,
|
||
|
// 2) after all entries were garbage collected, or
|
||
|
// 3) the freelist was emptied after the last entry was taken.
|
||
|
//
|
||
|
// In all of these cases, it is safe to pause the recycle ticker
|
||
|
// since it will be resumed as soon an entry is returned to the
|
||
|
// freelist.
|
||
|
if q.freeList.Len() == 0 {
|
||
|
q.freeList.PushBack(gcQueueEntry{
|
||
|
item: q.newItem(),
|
||
|
time: time.Now(),
|
||
|
})
|
||
|
|
||
|
q.recycleTicker.Pause()
|
||
|
}
|
||
|
|
||
|
next := q.freeList.Front()
|
||
|
|
||
|
select {
|
||
|
|
||
|
// If a client requests a new write buffer, deliver the buffer
|
||
|
// at the head of the freelist to them.
|
||
|
case q.takeBuffer <- next.Value.(gcQueueEntry).item:
|
||
|
q.freeList.Remove(next)
|
||
|
|
||
|
// If a client is returning a write buffer, add it to the free
|
||
|
// list and resume the recycle ticker so that it can be cleared
|
||
|
// if the entries are not quickly reused.
|
||
|
case item := <-q.returnBuffer:
|
||
|
// Add the returned buffer to the freelist, recording
|
||
|
// the current time so we can determine when the entry
|
||
|
// expires.
|
||
|
q.freeList.PushBack(gcQueueEntry{
|
||
|
item: item,
|
||
|
time: time.Now(),
|
||
|
})
|
||
|
|
||
|
// Adding the buffer implies that we now have a non-zero
|
||
|
// number of elements in the free list. Resume the
|
||
|
// recycle ticker to cleanup any entries that go unused.
|
||
|
q.recycleTicker.Resume()
|
||
|
|
||
|
// If the recycle ticker fires, we will aggresively release any
|
||
|
// write buffers in the freelist for which the expiryInterval
|
||
|
// has elapsed since their insertion. If after doing so, no
|
||
|
// elements remain, we will pause the recylce ticker.
|
||
|
case <-q.recycleTicker.Ticks():
|
||
|
// Since the insert time of all entries will be
|
||
|
// monotonically increasing, iterate over elements and
|
||
|
// remove all entries that have expired.
|
||
|
var next *list.Element
|
||
|
for e := q.freeList.Front(); e != nil; e = next {
|
||
|
// Cache the next element, since it will become
|
||
|
// unreachable from the current element if it is
|
||
|
// removed.
|
||
|
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 {
|
||
|
// If this entry hasn't expired, then
|
||
|
// all entries that follow will still be
|
||
|
// valid.
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|