github.com/decred/dcrlnd@v0.7.6/queue/gc_queue.go (about) 1 package queue 2 3 import ( 4 "container/list" 5 "time" 6 7 "github.com/decred/dcrlnd/ticker" 8 ) 9 10 // GCQueue is garbage collecting queue, which dynamically grows and contracts 11 // based on load. If the queue has items which have been returned, the queue 12 // will check every gcInterval amount of time to see if any elements are 13 // eligible to be released back to the runtime. Elements that have been in the 14 // queue for a duration of least expiryInterval will be released upon the next 15 // iteration of the garbage collection, thus the maximum amount of time an 16 // element remain in the queue is expiryInterval+gcInterval. The gc ticker will 17 // be disabled after all items in the queue have been taken or released to 18 // ensure that the GCQueue becomes quiescent, and imposes minimal overhead in 19 // the steady state. 20 type GCQueue struct { 21 // takeBuffer coordinates the delivery of items taken from the queue 22 // such that they are delivered to requesters. 23 takeBuffer chan interface{} 24 25 // returnBuffer coordinates the return of items back into the queue, 26 // where they will be kept until retaken or released. 27 returnBuffer chan interface{} 28 29 // newItem is a constructor, used to generate new elements if none are 30 // otherwise available for reuse. 31 newItem func() interface{} 32 33 // expiryInterval is the minimum amount of time an element will remain 34 // in the queue before being released. 35 expiryInterval time.Duration 36 37 // recycleTicker is a resumable ticker used to trigger a sweep to 38 // release elements that have been in the queue longer than 39 // expiryInterval. 40 recycleTicker ticker.Ticker 41 42 // freeList maintains a list of gcQueueEntries, sorted in order of 43 // increasing time of arrival. 44 freeList *list.List 45 46 quit chan struct{} 47 } 48 49 // NewGCQueue creates a new garbage collecting queue, which dynamically grows 50 // and contracts based on load. If the queue has items which have been returned, 51 // the queue will check every gcInterval amount of time to see if any elements 52 // are eligible to be released back to the runtime. Elements that have been in 53 // the queue for a duration of least expiryInterval will be released upon the 54 // next iteration of the garbage collection, thus the maximum amount of time an 55 // element remain in the queue is expiryInterval+gcInterval. The gc ticker will 56 // be disabled after all items in the queue have been taken or released to 57 // ensure that the GCQueue becomes quiescent, and imposes minimal overhead in 58 // the steady state. The returnQueueSize parameter is used to size the maximal 59 // number of items that can be returned without being dropped during large 60 // bursts in attempts to return items to the GCQUeue. 61 func NewGCQueue(newItem func() interface{}, returnQueueSize int, 62 gcInterval, expiryInterval time.Duration) *GCQueue { 63 64 q := &GCQueue{ 65 takeBuffer: make(chan interface{}), 66 returnBuffer: make(chan interface{}, returnQueueSize), 67 expiryInterval: expiryInterval, 68 freeList: list.New(), 69 recycleTicker: ticker.New(gcInterval), 70 newItem: newItem, 71 quit: make(chan struct{}), 72 } 73 74 go q.queueManager() 75 76 return q 77 } 78 79 // Take returns either a recycled element from the queue, or creates a new item 80 // if none are available. 81 func (q *GCQueue) Take() interface{} { 82 select { 83 case item := <-q.takeBuffer: 84 return item 85 case <-time.After(time.Millisecond): 86 return q.newItem() 87 } 88 } 89 90 // Return adds the returned item to freelist if the queue's returnBuffer has 91 // available capacity. Under load, items may be dropped to ensure this method 92 // does not block. 93 func (q *GCQueue) Return(item interface{}) { 94 select { 95 case q.returnBuffer <- item: 96 default: 97 } 98 } 99 100 // gcQueueEntry is a tuple containing an interface{} and the time at which the 101 // item was added to the queue. The recorded time is used to determine when the 102 // entry becomes stale, and can be released if it has not already been taken. 103 type gcQueueEntry struct { 104 item interface{} 105 time time.Time 106 } 107 108 // queueManager maintains the free list of elements by popping the head of the 109 // queue when items are needed, and appending them to the end of the queue when 110 // items are returned. The queueManager will periodically attempt to release any 111 // items that have been in the queue longer than the expiry interval. 112 // 113 // NOTE: This method SHOULD be run as a goroutine. 114 func (q *GCQueue) queueManager() { 115 for { 116 // If the pool is empty, initialize a buffer pool to serve a 117 // client that takes a buffer immediately. If this happens, this 118 // is either: 119 // 1) the first iteration of the loop, 120 // 2) after all entries were garbage collected, or 121 // 3) the freelist was emptied after the last entry was taken. 122 // 123 // In all of these cases, it is safe to pause the recycle ticker 124 // since it will be resumed as soon an entry is returned to the 125 // freelist. 126 if q.freeList.Len() == 0 { 127 q.freeList.PushBack(gcQueueEntry{ 128 item: q.newItem(), 129 time: time.Now(), 130 }) 131 132 q.recycleTicker.Pause() 133 } 134 135 next := q.freeList.Front() 136 137 select { 138 139 // If a client requests a new write buffer, deliver the buffer 140 // at the head of the freelist to them. 141 case q.takeBuffer <- next.Value.(gcQueueEntry).item: 142 q.freeList.Remove(next) 143 144 // If a client is returning a write buffer, add it to the free 145 // list and resume the recycle ticker so that it can be cleared 146 // if the entries are not quickly reused. 147 case item := <-q.returnBuffer: 148 // Add the returned buffer to the freelist, recording 149 // the current time so we can determine when the entry 150 // expires. 151 q.freeList.PushBack(gcQueueEntry{ 152 item: item, 153 time: time.Now(), 154 }) 155 156 // Adding the buffer implies that we now have a non-zero 157 // number of elements in the free list. Resume the 158 // recycle ticker to cleanup any entries that go unused. 159 q.recycleTicker.Resume() 160 161 // If the recycle ticker fires, we will aggresively release any 162 // write buffers in the freelist for which the expiryInterval 163 // has elapsed since their insertion. If after doing so, no 164 // elements remain, we will pause the recylce ticker. 165 case <-q.recycleTicker.Ticks(): 166 // Since the insert time of all entries will be 167 // monotonically increasing, iterate over elements and 168 // remove all entries that have expired. 169 var next *list.Element 170 for e := q.freeList.Front(); e != nil; e = next { 171 // Cache the next element, since it will become 172 // unreachable from the current element if it is 173 // removed. 174 next = e.Next() 175 entry := e.Value.(gcQueueEntry) 176 177 // Use now - insertTime <= expiryInterval to 178 // determine if this entry has not expired. 179 if time.Since(entry.time) <= q.expiryInterval { 180 // If this entry hasn't expired, then 181 // all entries that follow will still be 182 // valid. 183 break 184 } 185 186 // Otherwise, remove the expired entry from the 187 // linked-list. 188 q.freeList.Remove(e) 189 entry.item = nil 190 e.Value = nil 191 } 192 } 193 } 194 }