grafana/pkg/util/ring/ring.go
Diego Augusto Molina e6ead667b3
Unified Storage: added pkg/util/ring package to handle queueing of notifications (#84657)
* added pkg/util/rinq package to handle queueing of notifications

* fix linters

* Fix typo in comment

Co-authored-by: Dan Cech <dcech@grafana.com>

* improve allocation strategy for Enqueue; remove unnecessary clearing of slice

* Update pkg/util/ringq/dyn_chan_bench_test.go

Co-authored-by: Dan Cech <dcech@grafana.com>

* Update pkg/util/ringq/ringq.go

Co-authored-by: Dan Cech <dcech@grafana.com>

* refactor to move stats and shrinking into Ring

* add missing error assertions in tests

* add missing error assertions in tests and linting issues

* simplify controller closed check

* improve encapsulation of internal state in Ring

* use (*Ring).Len for clarity instead of stats

---------

Co-authored-by: Dan Cech <dcech@grafana.com>
2024-04-11 19:32:31 -03:00

255 lines
7.3 KiB
Go

package ring
// Ring is a ring buffer backed by a slice that rearranges itself to grow and
// shrink as needed. It can also be grown and shrunk manually. It is not safe
// for concurrent use, and the zero value is ready for use. Dequeued and cleared
// items are zeroed in the underlying slice to release references and allow
// garbage collection. Leaving growth and shrinkage of the internal slice apart,
// which can be directly controlled, all operations are allocation free.
type Ring[T any] struct {
buf []T
stats RingStats
back, len int
// Min sets the minimum capacity that the Ring can have, and takes effect
// in the next write operation. The Ring will naturally tend to shrink
// towards Min when free capacity allows it. Setting this value has no
// immediate effect, but instead affects future writing operations. Min is
// valid only if:
// 0 < Min && ( Max <= 0 || Min <= Max )
// Note that this allows setting Min but not Max, or setting both as well.
Min int
// Max sets the maximum capacity that the Ring can grow to store new items.
// Setting this value has no immediate effect, but instead affects future
// writing operations. Max is valid only if:
// 0 < Max && Min <= Max
// Note that this allows setting Max but not Min, or setting both as well.
Max int
}
// RingStats provides general stats for a Ring.
type RingStats struct {
// Len is the used capacity.
Len int
// Cap is the current total capacity.
Cap int
// Grown is the number of times a larger buffer was allocated.
Grown uint64
// Shrunk is the number of times a smaller buffer was allocated.
Shrunk uint64
// Allocs is Grown + Shrunk.
Allocs uint64
// Enqueued is the total number of items entered into the Ring, including
// those which caused other items to be dropped.
Enqueued uint64
// Dequeued is the total number of items removed from the Ring, including
// items removed with Dequeue and with Clear.
Dequeued uint64
// Dropped is the number of items lost due to the Ring being at capacity.
Dropped uint64
}
// Len returns the used capacity.
func (rq *Ring[T]) Len() int {
return rq.len
}
// Cap returns the current total capacity.
func (rq *Ring[T]) Cap() int {
return len(rq.buf)
}
// WriteStats writes general stats about this Ring to the given *RingStats, if
// it's non-nil.
func (rq *Ring[T]) WriteStats(s *RingStats) {
if s == nil {
return
}
rq.stats.Len = rq.len
rq.stats.Cap = len(rq.buf)
*s = rq.stats
}
// Clear removes all items from the Ring and returns the number of items
// removed. If Min is valid and Cap() > Min, it will also shrink the capacity to
// Min. Stats are not cleared, but instead Dequeued is increased by the number
// of removed items.
func (rq *Ring[T]) Clear() int {
cleared := rq.len
rq.stats.Dequeued += uint64(cleared)
shouldMigrate := clearShouldMigrate(len(rq.buf), rq.Min, rq.Max)
if rq.len > 0 && !shouldMigrate {
// if we migrate we don't need to clear items, since moving to the new
// slice will just have the old slice garbage collected
chunk := min(rq.back+rq.len, len(rq.buf))
clear(rq.buf[rq.back:chunk])
clear(rq.buf[:rq.len-chunk])
}
rq.back = 0
rq.len = 0
if shouldMigrate {
rq.migrate(rq.Min)
}
return cleared
}
// Shrink makes sure free capacity is not greater than n, shrinking if
// necessary. If a new allocation is needed then it will be capped to Min, given
// than Min is valid.
func (rq *Ring[T]) Shrink(n int) {
if n < 0 || rq.len+n >= len(rq.buf) {
return
}
rq.migrate(n)
}
// Grow makes sure free capacity is at least n, growing if necessary. If a new
// allocation is needed then it will be capped to Max, given that Max is valid.
func (rq *Ring[T]) Grow(n int) {
if n < 1 || rq.len+n <= len(rq.buf) {
return
}
rq.migrate(n)
}
func (rq *Ring[T]) migrate(newFreeCap int) {
newCap := rq.len + newFreeCap
newCap = fixAllocSize(rq.len, rq.Min, rq.Max, newCap)
if newCap == len(rq.buf) {
return
}
var s []T
if newCap > 0 {
// if newCap == 0 then just set rq.s to nil
s = make([]T, newCap)
}
if len(s) > len(rq.buf) {
rq.stats.Grown++
} else {
rq.stats.Shrunk++
}
if rq.len > 0 {
chunk1 := min(rq.back+rq.len, len(rq.buf))
copied := copy(s, rq.buf[rq.back:chunk1])
if copied < rq.len {
// wrapped the slice
chunk2 := rq.len - copied
copy(s[copied:], rq.buf[:chunk2])
}
}
rq.back = 0
rq.buf = s
}
// Enqueue adds the given item to the Ring, growing the capacity if needed. If
// the Ring is at capacity (0 < Max && Min <= Max && rq.Len() == rq.Cap()),
// then the new item will overwrite the oldest enqueued item.
func (rq *Ring[T]) Enqueue(v T) {
// try to add space if we're at capacity or fix min allocation
if rq.len == len(rq.buf) || (minIsValid(rq.Min, rq.Max) && len(rq.buf) < rq.Min) {
newFreeCap := rq.len + 1
newFreeCap = newFreeCap*3/2 + 1 // classic append: https://go.dev/blog/slices
newFreeCap -= rq.len // migrate only takes free capacity
rq.migrate(newFreeCap)
// if growing was capped at max, then overwrite the first item to be
// dequeued
if rq.len == len(rq.buf) {
rq.stats.Dropped++
rq.len--
if rq.back++; rq.back >= len(rq.buf) {
rq.back = 0 // wrap the slice
}
}
}
writePos := rq.back + rq.len
if writePos >= len(rq.buf) {
writePos -= len(rq.buf)
}
rq.buf[writePos] = v
rq.len++
rq.stats.Enqueued++
}
// Peek is like Dequeue, but it doesn't remove the item.
func (rq *Ring[T]) Peek() (v T) {
if rq.len == 0 {
return
}
return rq.buf[rq.back]
}
// Dequeue removes the oldest enqueued item and returns it. If the Ring is
// empty, it returns the zero value.
func (rq *Ring[T]) Dequeue() (v T) {
if rq.len == 0 {
return
}
// get the value into v, and also zero out the slice item to release
// references so they can be gc'd
v, rq.buf[rq.back] = rq.buf[rq.back], v
rq.len--
if rq.back++; rq.back >= len(rq.buf) {
rq.back = 0 // wrap the slice
}
if minIsValid(rq.Min, rq.Max) && rq.len < len(rq.buf)/2+1 {
newFreeCap := len(rq.buf)*2/3 + 1 // opposite of growing arithmetic
newFreeCap -= rq.len // migrate only takes free capacity
rq.migrate(newFreeCap)
}
rq.stats.Dequeued++
return v
}
// the following functions provide small checks and arithmetics that are far
// easier to test separately than creating big and more complex tests covering a
// huge amount of combinatory options. This reduces the complexity of higher
// level tests and leaves only higher level logic, but also allows us to provide
// high coverage for even the most rare edge and boundary cases by adding a new
// line to the test cases table. They're also inlineable, so no penalty in
// calling them.
func minIsValid(Min, Max int) bool {
return 0 < Min && (Max <= 0 || Min <= Max)
}
func maxIsValid(Min, Max int) bool {
return 0 < Max && Min <= Max
}
func clearShouldMigrate(CurCap, Min, Max int) bool {
return minIsValid(Min, Max) && CurCap > Min
}
// fixAllocSize is a helper to determine what should be the new size to be
// allocated for a new slice, given the intended NewCap and the current relevant
// state of Ring. This is expected to be called inside (*Ring).migrate.
func fixAllocSize(CurLen, Min, Max, NewCap int) int {
if minIsValid(Min, Max) { // Min is valid
NewCap = max(NewCap, CurLen, Min)
} else {
NewCap = max(CurLen, NewCap)
}
if maxIsValid(Min, Max) { // Max is valid
NewCap = min(NewCap, Max)
}
return NewCap
}