mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-07 04:41:31 -05:00
287 lines
7.8 KiB
Go
287 lines
7.8 KiB
Go
// Copyright 2021-2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
const ipQueueDefaultMaxRecycleSize = 4 * 1024
|
|
|
|
// This is a generic intra-process queue.
|
|
type ipQueue[T any] struct {
|
|
inprogress int64
|
|
sync.Mutex
|
|
ch chan struct{}
|
|
elts []T
|
|
pos int
|
|
pool *sync.Pool
|
|
sz uint64 // Calculated size (only if calc != nil)
|
|
name string
|
|
m *sync.Map
|
|
ipQueueOpts[T]
|
|
}
|
|
|
|
type ipQueueOpts[T any] struct {
|
|
mrs int // Max recycle size
|
|
calc func(e T) uint64 // Calc function for tracking size
|
|
msz uint64 // Limit by total calculated size
|
|
mlen int // Limit by number of entries
|
|
}
|
|
|
|
type ipQueueOpt[T any] func(*ipQueueOpts[T])
|
|
|
|
// This option allows to set the maximum recycle size when attempting
|
|
// to put back a slice to the pool.
|
|
func ipqMaxRecycleSize[T any](max int) ipQueueOpt[T] {
|
|
return func(o *ipQueueOpts[T]) {
|
|
o.mrs = max
|
|
}
|
|
}
|
|
|
|
// This option enables total queue size counting by passing in a function
|
|
// that evaluates the size of each entry as it is pushed/popped. This option
|
|
// enables the size() function.
|
|
func ipqSizeCalculation[T any](calc func(e T) uint64) ipQueueOpt[T] {
|
|
return func(o *ipQueueOpts[T]) {
|
|
o.calc = calc
|
|
}
|
|
}
|
|
|
|
// This option allows setting the maximum queue size. Once the limit is
|
|
// reached, then push() will stop returning true and no more entries will
|
|
// be stored until some more are popped. The ipQueue_SizeCalculation must
|
|
// be provided for this to work.
|
|
func ipqLimitBySize[T any](max uint64) ipQueueOpt[T] {
|
|
return func(o *ipQueueOpts[T]) {
|
|
o.msz = max
|
|
}
|
|
}
|
|
|
|
// This option allows setting the maximum queue length. Once the limit is
|
|
// reached, then push() will stop returning true and no more entries will
|
|
// be stored until some more are popped.
|
|
func ipqLimitByLen[T any](max int) ipQueueOpt[T] {
|
|
return func(o *ipQueueOpts[T]) {
|
|
o.mlen = max
|
|
}
|
|
}
|
|
|
|
var errIPQLenLimitReached = errors.New("IPQ len limit reached")
|
|
var errIPQSizeLimitReached = errors.New("IPQ size limit reached")
|
|
|
|
func newIPQueue[T any](s *Server, name string, opts ...ipQueueOpt[T]) *ipQueue[T] {
|
|
q := &ipQueue[T]{
|
|
ch: make(chan struct{}, 1),
|
|
pool: &sync.Pool{
|
|
New: func() any {
|
|
// Reason we use pointer to slice instead of slice is explained
|
|
// here: https://staticcheck.io/docs/checks#SA6002
|
|
res := make([]T, 0, 32)
|
|
return &res
|
|
},
|
|
},
|
|
name: name,
|
|
m: &s.ipQueues,
|
|
ipQueueOpts: ipQueueOpts[T]{
|
|
mrs: ipQueueDefaultMaxRecycleSize,
|
|
},
|
|
}
|
|
for _, o := range opts {
|
|
o(&q.ipQueueOpts)
|
|
}
|
|
s.ipQueues.Store(name, q)
|
|
return q
|
|
}
|
|
|
|
// Add the element `e` to the queue, notifying the queue channel's `ch` if the
|
|
// entry is the first to be added, and returns the length of the queue after
|
|
// this element is added.
|
|
func (q *ipQueue[T]) push(e T) (int, error) {
|
|
q.Lock()
|
|
l := len(q.elts) - q.pos
|
|
if q.mlen > 0 && l == q.mlen {
|
|
q.Unlock()
|
|
return l, errIPQLenLimitReached
|
|
}
|
|
if q.calc != nil {
|
|
sz := q.calc(e)
|
|
if q.msz > 0 && q.sz+sz > q.msz {
|
|
q.Unlock()
|
|
return l, errIPQSizeLimitReached
|
|
}
|
|
q.sz += sz
|
|
}
|
|
if q.elts == nil {
|
|
// What comes out of the pool is already of size 0, so no need for [:0].
|
|
q.elts = *(q.pool.Get().(*[]T))
|
|
}
|
|
q.elts = append(q.elts, e)
|
|
q.Unlock()
|
|
if l == 0 {
|
|
select {
|
|
case q.ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
return l + 1, nil
|
|
}
|
|
|
|
// Returns the whole list of elements currently present in the queue,
|
|
// emptying the queue. This should be called after receiving a notification
|
|
// from the queue's `ch` notification channel that indicates that there
|
|
// is something in the queue.
|
|
// However, in cases where `drain()` may be called from another go
|
|
// routine, it is possible that a routine is notified that there is
|
|
// something, but by the time it calls `pop()`, the drain() would have
|
|
// emptied the queue. So the caller should never assume that pop() will
|
|
// return a slice of 1 or more, it could return `nil`.
|
|
func (q *ipQueue[T]) pop() []T {
|
|
if q == nil {
|
|
return nil
|
|
}
|
|
q.Lock()
|
|
if len(q.elts)-q.pos == 0 {
|
|
q.Unlock()
|
|
return nil
|
|
}
|
|
var elts []T
|
|
if q.pos == 0 {
|
|
elts = q.elts
|
|
} else {
|
|
elts = q.elts[q.pos:]
|
|
}
|
|
q.elts, q.pos, q.sz = nil, 0, 0
|
|
atomic.AddInt64(&q.inprogress, int64(len(elts)))
|
|
q.Unlock()
|
|
return elts
|
|
}
|
|
|
|
// Returns the first element from the queue, if any. See comment above
|
|
// regarding calling after being notified that there is something and
|
|
// the use of drain(). In short, the caller should always check the
|
|
// boolean return value to ensure that the value is genuine and not a
|
|
// default empty value.
|
|
func (q *ipQueue[T]) popOne() (T, bool) {
|
|
q.Lock()
|
|
l := len(q.elts) - q.pos
|
|
if l == 0 {
|
|
q.Unlock()
|
|
var empty T
|
|
return empty, false
|
|
}
|
|
e := q.elts[q.pos]
|
|
if l--; l > 0 {
|
|
q.pos++
|
|
if q.calc != nil {
|
|
q.sz -= q.calc(e)
|
|
}
|
|
// We need to re-signal
|
|
select {
|
|
case q.ch <- struct{}{}:
|
|
default:
|
|
}
|
|
} else {
|
|
// We have just emptied the queue, so we can reuse unless it is too big.
|
|
if cap(q.elts) <= q.mrs {
|
|
q.elts = q.elts[:0]
|
|
} else {
|
|
q.elts = nil
|
|
}
|
|
q.pos, q.sz = 0, 0
|
|
}
|
|
q.Unlock()
|
|
return e, true
|
|
}
|
|
|
|
// After a pop(), the slice can be recycled for the next push() when
|
|
// a first element is added to the queue.
|
|
// This will also decrement the "in progress" count with the length
|
|
// of the slice.
|
|
// WARNING: The caller MUST never reuse `elts`.
|
|
func (q *ipQueue[T]) recycle(elts *[]T) {
|
|
// If invoked with a nil list, nothing to do.
|
|
if elts == nil || *elts == nil {
|
|
return
|
|
}
|
|
// Update the in progress count.
|
|
if len(*elts) > 0 {
|
|
atomic.AddInt64(&q.inprogress, int64(-(len(*elts))))
|
|
}
|
|
// We also don't want to recycle huge slices, so check against the max.
|
|
// q.mrs is normally immutable but can be changed, in a safe way, in some tests.
|
|
if cap(*elts) > q.mrs {
|
|
return
|
|
}
|
|
(*elts) = (*elts)[:0]
|
|
q.pool.Put(elts)
|
|
}
|
|
|
|
// Returns the current length of the queue.
|
|
func (q *ipQueue[T]) len() int {
|
|
q.Lock()
|
|
defer q.Unlock()
|
|
return len(q.elts) - q.pos
|
|
}
|
|
|
|
// Returns the calculated size of the queue (if ipQueue_SizeCalculation has been
|
|
// passed in), otherwise returns zero.
|
|
func (q *ipQueue[T]) size() uint64 {
|
|
q.Lock()
|
|
defer q.Unlock()
|
|
return q.sz
|
|
}
|
|
|
|
// Empty the queue and consumes the notification signal if present.
|
|
// Returns the number of items that were drained from the queue.
|
|
// Note that this could cause a reader go routine that has been
|
|
// notified that there is something in the queue (reading from queue's `ch`)
|
|
// may then get nothing if `drain()` is invoked before the `pop()` or `popOne()`.
|
|
func (q *ipQueue[T]) drain() int {
|
|
if q == nil {
|
|
return 0
|
|
}
|
|
q.Lock()
|
|
olen := len(q.elts) - q.pos
|
|
q.elts, q.pos, q.sz = nil, 0, 0
|
|
// Consume the signal if it was present to reduce the chance of a reader
|
|
// routine to be think that there is something in the queue...
|
|
select {
|
|
case <-q.ch:
|
|
default:
|
|
}
|
|
q.Unlock()
|
|
return olen
|
|
}
|
|
|
|
// Since the length of the queue goes to 0 after a pop(), it is good to
|
|
// have an insight on how many elements are yet to be processed after a pop().
|
|
// For that reason, the queue maintains a count of elements returned through
|
|
// the pop() API. When the caller will call q.recycle(), this count will
|
|
// be reduced by the size of the slice returned by pop().
|
|
func (q *ipQueue[T]) inProgress() int64 {
|
|
return atomic.LoadInt64(&q.inprogress)
|
|
}
|
|
|
|
// Remove this queue from the server's map of ipQueues.
|
|
// All ipQueue operations (such as push/pop/etc..) are still possible.
|
|
func (q *ipQueue[T]) unregister() {
|
|
if q == nil {
|
|
return
|
|
}
|
|
q.m.Delete(q.name)
|
|
}
|