mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 05:02:21 +08:00

* 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>
286 lines
8.8 KiB
Go
286 lines
8.8 KiB
Go
package ring
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// Package level named errors.
|
|
var (
|
|
ErrAdaptiveChanClosed = errors.New("closed AdaptiveChan")
|
|
ErrAdaptiveChanControllerClosed = errors.New("closed AdaptiveChanController")
|
|
)
|
|
|
|
// AdaptiveChan provides a queueing system based on a send-only, a receive-only,
|
|
// and an internal ring buffer queue backed by a *Ring. It also provides an
|
|
// AdaptiveChanController to provide stats and some control on the internal
|
|
// *Ring. Termination is controlled by closing the returned send-only channel.
|
|
// After doing so, the receive-only channel will have the chance to receive all
|
|
// the items still in the queue and will be immediately closed afterwards. Once
|
|
// both channels are closed, the AdaptiveChanController will no longer be usable
|
|
// and will only return ErrAdaptiveChanClosed for all its methods. Leaving the
|
|
// growth and shrinkage of the internal *Ring apart, which can be controlled
|
|
// with the AdaptiveChanController, the implementation is allocation free.
|
|
//
|
|
// The implementation explicitly returns two channels and a struct, instead of
|
|
// just one struct that has the channels, to make a clear statement about the
|
|
// intended usage pattern:
|
|
//
|
|
// 1. Create an adaptive channel.
|
|
// 2. Provide the send-only channel to your producer(s). They are responsible
|
|
// for closing this channel when they're done. If more than one goroutine
|
|
// will have access to this channel, then it's the producer's responsibility
|
|
// to coordinate the channel close operation.
|
|
// 3. Provide the receive-only channel to your consumer(s), and let them
|
|
// receive from it with the two return value syntax for channels in order to
|
|
// check for termination from the sending side.
|
|
// 4. Use the AdaptiveChanController to control the internal buffer behaviour
|
|
// and to monitor stats. This should typically be held by the creator of the
|
|
// adaptive channel. Refrain from holding a reference to the send-only
|
|
// channel to force termination of the producing side. Instead, provide a
|
|
// side mechanism to communicate the intention of terminating the sending
|
|
// side, e.g. providing your producer(s) with a context as well as the
|
|
// send-only channel. An adaptive channel is meant as a queueing system, not
|
|
// as a coordination mechanism for producer(s), consumer(s) and
|
|
// controller(s).
|
|
//
|
|
// This pattern is designed to maximize decoupling while providing insights and
|
|
// granular control on memory usage. While the controller is not meant to make
|
|
// any direct changes to the queued data, the Clear method provides the
|
|
// opportunity to discard all queued items as an administrative measure. This
|
|
// doesn't terminate the queue, though, i.e. it doesn't close the send-only
|
|
// channel.
|
|
func AdaptiveChan[T any]() (send chan<- T, recv <-chan T, ctrl *AdaptiveChanController) {
|
|
internalSend := make(chan T)
|
|
internalRecv := make(chan T)
|
|
statsChan := make(chan AdaptiveChanStats)
|
|
cmdChan := make(chan acCmd)
|
|
ctrl = &AdaptiveChanController{
|
|
statsChan: statsChan,
|
|
cmdChan: cmdChan,
|
|
}
|
|
|
|
go func() {
|
|
defer close(internalRecv)
|
|
defer close(statsChan)
|
|
|
|
var q Ring[T]
|
|
var stats AdaptiveChanStats
|
|
|
|
// the loop condition is that we either have items to dequeue or that we
|
|
// have the possibility to receive new items to be queued
|
|
for q.Len() > 0 || internalSend != nil {
|
|
// NOTE: the overhead of writing stats in each iteration is
|
|
// negligible. I tried a two phase stats writing with a chan
|
|
// struct{} to get notified that the controller wanted stats, then
|
|
// updating the stats and finally writing to statsChan. There was no
|
|
// observable difference for just enqueueing and dequeueing after
|
|
// running the benchmarks several times, and reading stats got worse
|
|
// by ~22%
|
|
q.WriteStats(&stats.RingStats)
|
|
|
|
// if we don't have anything in the queue, then make the dequeueing
|
|
// branch of the select block indefinitely by providing a nil
|
|
// channel, leaving only the queueing branch available
|
|
dequeueChan := internalRecv
|
|
if q.Len() == 0 {
|
|
dequeueChan = nil
|
|
}
|
|
|
|
select {
|
|
case v, ok := <-internalSend: // blocks until something is queued
|
|
if !ok {
|
|
// in was closed, so if we leave it like that the next
|
|
// iteration will keep receiving zero values with ok=false
|
|
// without any blocking. So we set in to nil, so that
|
|
// the next iteration the select will block indefinitely on
|
|
// this branch of the select and leave only the dequeing
|
|
// branch active until all items have been dequeued
|
|
internalSend = nil
|
|
} else {
|
|
q.Enqueue(v)
|
|
}
|
|
|
|
case dequeueChan <- q.Peek(): // blocks if nothing to dequeue
|
|
// we don't want to call Dequeue in the `case` above since that
|
|
// would consume the item and it would be lost if the queueing
|
|
// branch was selected, so instead we Peek in the `case` and we
|
|
// do the actual dequeueing here once we know this branch was
|
|
// selected
|
|
q.Dequeue()
|
|
|
|
case statsChan <- stats:
|
|
// stats reading
|
|
stats.StatsRead++
|
|
|
|
case cmd, ok := <-cmdChan:
|
|
if !ok {
|
|
// AdaptiveChanController was closed. Set cmdChan to nil so
|
|
// this branch blocks in the next iteration
|
|
cmdChan = nil
|
|
continue
|
|
}
|
|
|
|
// execute a command on the internal *Ring
|
|
switch cmd.acCmdType {
|
|
case acCmdMin:
|
|
q.Min = cmd.intValue
|
|
stats.Min = cmd.intValue
|
|
case acCmdMax:
|
|
q.Max = cmd.intValue
|
|
stats.Max = cmd.intValue
|
|
case acCmdGrow:
|
|
q.Grow(cmd.intValue)
|
|
case acCmdShrink:
|
|
q.Shrink(cmd.intValue)
|
|
case acCmdClear:
|
|
q.Clear()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return internalSend, internalRecv, ctrl
|
|
}
|
|
|
|
type acCmdType uint8
|
|
|
|
const (
|
|
acCmdMin = iota
|
|
acCmdMax
|
|
acCmdClear
|
|
acCmdGrow
|
|
acCmdShrink
|
|
)
|
|
|
|
type acCmd struct {
|
|
acCmdType
|
|
intValue int
|
|
}
|
|
|
|
// AdaptiveChanController provides access to an AdaptiveChan's internal *Ring.
|
|
type AdaptiveChanController struct {
|
|
statsChan <-chan AdaptiveChanStats
|
|
cmdChan chan<- acCmd
|
|
cmdChanMu sync.Mutex
|
|
}
|
|
|
|
// Close releases resources associated with this controller. After calling this
|
|
// method, all other methods will return ErrAdaptiveChanControllerClosed. It is
|
|
// idempotent. This doesn't affect the queue itself, but rather prevents further
|
|
// administrative tasks to be performed through the AdaptiveChanController.
|
|
func (r *AdaptiveChanController) Close() {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
if r.cmdChan != nil {
|
|
close(r.cmdChan)
|
|
r.cmdChan = nil
|
|
}
|
|
}
|
|
|
|
// Min sets the value of Min in the internal *Ring.
|
|
func (r *AdaptiveChanController) Min(ctx context.Context, n int) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
|
acCmdType: acCmdMin,
|
|
intValue: n,
|
|
})
|
|
}
|
|
|
|
// Max sets the value of Max in the internal *Ring.
|
|
func (r *AdaptiveChanController) Max(ctx context.Context, n int) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
|
acCmdType: acCmdMax,
|
|
intValue: n,
|
|
})
|
|
}
|
|
|
|
// Grow calls Grow on the internal *Ring.
|
|
func (r *AdaptiveChanController) Grow(ctx context.Context, n int) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
|
acCmdType: acCmdGrow,
|
|
intValue: n,
|
|
})
|
|
}
|
|
|
|
// Shrink calls Shrink on the internal *Ring.
|
|
func (r *AdaptiveChanController) Shrink(ctx context.Context, n int) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
|
acCmdType: acCmdShrink,
|
|
intValue: n,
|
|
})
|
|
}
|
|
|
|
// Clear calls Clear on the internal *Ring.
|
|
func (r *AdaptiveChanController) Clear(ctx context.Context) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
|
|
return sendOrErr(ctx, r.cmdChan, acCmd{
|
|
acCmdType: acCmdClear,
|
|
})
|
|
}
|
|
|
|
// WriteStats writes a snapshot of general stats about the associated
|
|
// AdaptiveChan to the given *AdaptiveChanStats.
|
|
func (r *AdaptiveChanController) WriteStats(ctx context.Context, s *AdaptiveChanStats) error {
|
|
r.cmdChanMu.Lock()
|
|
defer r.cmdChanMu.Unlock()
|
|
if r.cmdChan == nil {
|
|
return ErrAdaptiveChanControllerClosed
|
|
}
|
|
|
|
return recvOrErr(ctx, r.statsChan, s)
|
|
}
|
|
|
|
// AdaptiveChanStats is a snapshot of general stats for an AdaptiveChan.
|
|
type AdaptiveChanStats struct {
|
|
RingStats
|
|
// Min is the value of Min in the internal *Ring.
|
|
Min int
|
|
// Max value of Max in the internal *Ring.
|
|
Max int
|
|
// StatsRead is the total number of stats read before this snapshot. If it
|
|
// is zero, it means this snapshot is the first reading.
|
|
StatsRead uint64
|
|
}
|
|
|
|
func sendOrErr[T any](ctx context.Context, c chan<- T, v T) error {
|
|
if c == nil {
|
|
return ErrAdaptiveChanControllerClosed
|
|
}
|
|
|
|
select {
|
|
case c <- v:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func recvOrErr[T any](ctx context.Context, c <-chan T, tptr *T) error {
|
|
select {
|
|
case t, ok := <-c:
|
|
if !ok {
|
|
return ErrAdaptiveChanClosed
|
|
}
|
|
*tptr = t
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|