663 lines
15 KiB
Go
663 lines
15 KiB
Go
// Copyright 2025 PingCAP, Inc.
|
|
//
|
|
// 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 memory
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var resourcePoolID = int64(-1)
|
|
|
|
// DefPoolAllocAlignSize indicates the default allocation alignment size
|
|
const DefPoolAllocAlignSize int64 = 10 * 1024
|
|
|
|
// DefMaxUnusedBlocks indicates the default maximum unused blocks*alloc-align-size of the resource pool
|
|
const DefMaxUnusedBlocks int64 = 10
|
|
|
|
// ResourcePool manages a set of resource quota
|
|
type ResourcePool struct {
|
|
actions PoolActions // actions to be taken when the pool meets certain conditions
|
|
parentMu struct{ prevChildren, nextChildren *ResourcePool } // accessible by parent pool only
|
|
name string // name of pool
|
|
mu struct {
|
|
headChildren *ResourcePool // head of the children pools chain
|
|
budget Budget // budget of the resource quota
|
|
allocated int64 // allocated bytes of the resource quota
|
|
maxAllocated int64 // maximum allocated bytes
|
|
numChildren int // number of children pools
|
|
sync.Mutex
|
|
stopped bool
|
|
}
|
|
uid uint64 // unique ID of the resource pool: uid <= 0 indicates that it is a internal pool
|
|
reserved int64 // quota from other sources
|
|
limit int64 // limit of the resource quota
|
|
allocAlignSize int64 // each allocation size must be a multiple of it
|
|
maxUnusedBlocks int64 // max unused-blocks*alloc-align size before shrinking budget
|
|
}
|
|
|
|
// NoteActionState wraps the arguments of a note action
|
|
type NoteActionState struct {
|
|
Pool *ResourcePool
|
|
Allocated int64
|
|
}
|
|
|
|
// NoteAction represents the action to be taken when the allocated size exceeds the threshold
|
|
type NoteAction struct {
|
|
CB func(NoteActionState)
|
|
Threshold int64
|
|
}
|
|
|
|
// OutOfCapacityActionArgs wraps the arguments for out of capacity action
|
|
type OutOfCapacityActionArgs struct {
|
|
Pool *ResourcePool
|
|
Request int64
|
|
}
|
|
|
|
// PoolActions represents the actions to be taken when the resource pool meets certain conditions
|
|
type PoolActions struct {
|
|
OutOfCapacityActionCB func(OutOfCapacityActionArgs) error // Called when the resource pool is out of capacity
|
|
OutOfLimitActionCB func(*ResourcePool) error // Called when the resource pool is out of limit
|
|
NoteAction NoteAction
|
|
}
|
|
|
|
// ResourcePoolState represents the state of a resource pool
|
|
type ResourcePoolState struct {
|
|
Name string
|
|
Level int
|
|
ID uint64
|
|
ParentID uint64
|
|
Used int64
|
|
Reserved int64
|
|
Budget int64
|
|
}
|
|
|
|
// Traverse the resource pool and calls the callback function
|
|
func (p *ResourcePool) Traverse(stateCb func(ResourcePoolState) error) error {
|
|
return p.traverse(0, stateCb)
|
|
}
|
|
|
|
func (p *ResourcePool) traverse(level int, stateCb func(ResourcePoolState) error) error {
|
|
p.mu.Lock()
|
|
if p.mu.stopped {
|
|
p.mu.Unlock()
|
|
return nil
|
|
}
|
|
monitorState := ResourcePoolState{
|
|
Level: level,
|
|
Name: p.name,
|
|
ID: p.uid,
|
|
ParentID: p.mu.budget.pool.UID(),
|
|
Used: p.mu.allocated,
|
|
Reserved: p.reserved,
|
|
Budget: p.mu.budget.cap,
|
|
}
|
|
children := make([]*ResourcePool, 0, p.mu.numChildren)
|
|
for c := p.mu.headChildren; c != nil; c = c.parentMu.nextChildren {
|
|
children = append(children, c)
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
if err := stateCb(monitorState); err != nil {
|
|
return err
|
|
}
|
|
for _, c := range children {
|
|
if err := c.traverse(level+1, stateCb); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewResourcePoolDefault creates a new resource pool
|
|
func NewResourcePoolDefault(
|
|
name string,
|
|
allocAlignSize int64,
|
|
) *ResourcePool {
|
|
return NewResourcePool(
|
|
newPoolUID(),
|
|
name,
|
|
0,
|
|
allocAlignSize,
|
|
DefMaxUnusedBlocks,
|
|
PoolActions{},
|
|
)
|
|
}
|
|
|
|
func newPoolUID() uint64 {
|
|
return uint64(atomic.AddInt64(&resourcePoolID, -1))
|
|
}
|
|
|
|
// NewResourcePool creates a new resource pool
|
|
func NewResourcePool(
|
|
uid uint64,
|
|
name string,
|
|
limit int64,
|
|
allocAlignSize int64,
|
|
maxUnusedBlocks int64,
|
|
actions PoolActions,
|
|
) *ResourcePool {
|
|
if allocAlignSize <= 0 {
|
|
allocAlignSize = DefPoolAllocAlignSize
|
|
}
|
|
if limit <= 0 {
|
|
limit = DefMaxLimit
|
|
}
|
|
m := &ResourcePool{
|
|
name: name,
|
|
uid: uid,
|
|
limit: limit,
|
|
allocAlignSize: allocAlignSize,
|
|
actions: actions,
|
|
maxUnusedBlocks: maxUnusedBlocks,
|
|
}
|
|
return m
|
|
}
|
|
|
|
// SetAllocAlignSize sets the allocation alignment size and returns the original value of allocAlignSize
|
|
func (p *ResourcePool) SetAllocAlignSize(size int64) (ori int64) {
|
|
p.mu.Lock()
|
|
ori = p.allocAlignSize
|
|
p.allocAlignSize = size
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// NewResourcePoolInheritWithLimit creates a new resource pool inheriting from the parent pool
|
|
func (p *ResourcePool) NewResourcePoolInheritWithLimit(
|
|
name string, limit int64,
|
|
) *ResourcePool {
|
|
return NewResourcePool(
|
|
newPoolUID(),
|
|
name,
|
|
limit,
|
|
p.allocAlignSize,
|
|
p.maxUnusedBlocks,
|
|
p.actions,
|
|
)
|
|
}
|
|
|
|
// StartNoReserved creates a new resource pool with no reserved quota
|
|
func (p *ResourcePool) StartNoReserved(pool *ResourcePool) {
|
|
p.Start(pool, 0)
|
|
}
|
|
|
|
// UID returns the unique ID of the resource pool
|
|
func (p *ResourcePool) UID() uint64 {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return p.uid
|
|
}
|
|
|
|
// Start starts the resource pool with a parent pool and reserved quota
|
|
func (p *ResourcePool) Start(parentPool *ResourcePool, reserved int64) {
|
|
if p.mu.allocated != 0 {
|
|
panic(fmt.Errorf("%s: started with %d bytes left over", p.name, p.mu.allocated))
|
|
}
|
|
if p.mu.budget.pool != nil {
|
|
panic(fmt.Errorf("%s: already started with pool %s", p.name, p.mu.budget.pool.name))
|
|
}
|
|
p.mu.allocated = 0
|
|
p.mu.maxAllocated = 0
|
|
p.mu.budget = parentPool.CreateBudget()
|
|
p.mu.stopped = false
|
|
p.reserved = reserved
|
|
|
|
if parentPool != nil {
|
|
parentPool.mu.Lock()
|
|
if s := parentPool.mu.headChildren; s != nil {
|
|
s.parentMu.prevChildren = p
|
|
p.parentMu.nextChildren = s
|
|
}
|
|
parentPool.mu.headChildren = p
|
|
parentPool.mu.numChildren++
|
|
parentPool.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Name returns the name of the resource pool
|
|
func (p *ResourcePool) Name() string {
|
|
return p.name
|
|
}
|
|
|
|
// Limit returns the limit of the resource pool
|
|
func (p *ResourcePool) Limit() int64 {
|
|
return p.limit
|
|
}
|
|
|
|
// IsStopped checks if the resource pool is stopped
|
|
func (p *ResourcePool) IsStopped() (res bool) {
|
|
p.mu.Lock()
|
|
res = p.mu.stopped
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Stop stops the resource pool and releases the budget & returns the quota released
|
|
func (p *ResourcePool) Stop() (released int64) {
|
|
p.mu.Lock()
|
|
|
|
p.mu.stopped = true
|
|
|
|
if p.mu.allocated != 0 {
|
|
p.doRelease(p.mu.allocated)
|
|
}
|
|
|
|
released = p.mu.budget.cap
|
|
p.releaseBudget()
|
|
|
|
if parent := p.mu.budget.pool; parent != nil {
|
|
func() {
|
|
parent.mu.Lock()
|
|
defer parent.mu.Unlock()
|
|
prev, next := p.parentMu.prevChildren, p.parentMu.nextChildren
|
|
if parent.mu.headChildren == p {
|
|
parent.mu.headChildren = next
|
|
}
|
|
if prev != nil {
|
|
prev.parentMu.nextChildren = next
|
|
}
|
|
if next != nil {
|
|
next.parentMu.prevChildren = prev
|
|
}
|
|
parent.mu.numChildren--
|
|
}()
|
|
}
|
|
|
|
p.mu.budget.pool = nil
|
|
|
|
p.mu.Unlock()
|
|
|
|
return released
|
|
}
|
|
|
|
// MaxAllocated returns the maximum allocated bytes
|
|
func (p *ResourcePool) MaxAllocated() (res int64) {
|
|
p.mu.Lock()
|
|
res = p.mu.maxAllocated
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Allocated returns the allocated bytes
|
|
func (p *ResourcePool) Allocated() (res int64) {
|
|
p.mu.Lock()
|
|
res = p.mu.allocated
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// ApproxAllocated returns the approximate allocated bytes
|
|
func (p *ResourcePool) ApproxAllocated() int64 {
|
|
return p.allocated()
|
|
}
|
|
|
|
// Budget represents the budget of a resource pool
|
|
type Budget struct {
|
|
pool *ResourcePool // source pool
|
|
cap int64 // capacity of the budget
|
|
used int64 // used bytes
|
|
explicitReserved int64 // explicit reserved size which can not be shrunk
|
|
}
|
|
|
|
// Used returns the used bytes of the budget
|
|
func (b *Budget) Used() int64 {
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
return b.used
|
|
}
|
|
|
|
// Pool returns the resource pool of the budget
|
|
func (b *Budget) Pool() *ResourcePool {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
return b.pool
|
|
}
|
|
|
|
// Capacity returns the capacity of the budget
|
|
func (b *Budget) Capacity() int64 {
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
return b.cap
|
|
}
|
|
|
|
func (b *Budget) available() int64 {
|
|
return b.cap - b.used
|
|
}
|
|
|
|
func (b *Budget) release() (reclaimed int64) {
|
|
reclaimed = b.available()
|
|
b.cap = b.used
|
|
return
|
|
}
|
|
|
|
// CreateBudget creates a new budget from the resource pool
|
|
func (p *ResourcePool) CreateBudget() Budget {
|
|
return Budget{pool: p}
|
|
}
|
|
|
|
// Reserve reserves the budget through the allocate aligned given size; update the explicit reserved size;
|
|
func (b *Budget) Reserve(request int64) error {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
minExtra := b.pool.roundSize(request)
|
|
if err := b.pool.allocate(minExtra); err != nil {
|
|
return err
|
|
}
|
|
b.cap += minExtra
|
|
b.explicitReserved += request
|
|
return nil
|
|
}
|
|
|
|
// Empty releases the used budget
|
|
func (b *Budget) Empty() {
|
|
if b == nil {
|
|
return
|
|
}
|
|
b.used = 0
|
|
if release := b.available() - b.pool.allocAlignSize; release > 0 {
|
|
b.pool.release(release)
|
|
b.cap -= release
|
|
}
|
|
}
|
|
|
|
// Clear releases the budget and resets
|
|
func (b *Budget) Clear() {
|
|
if b == nil {
|
|
return
|
|
}
|
|
release := b.cap
|
|
b.used = 0
|
|
b.cap = 0
|
|
|
|
if b.pool == nil {
|
|
return
|
|
}
|
|
if release > 0 {
|
|
b.pool.release(release)
|
|
}
|
|
}
|
|
|
|
func (b *Budget) resize(oldSz, newSz int64) error {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
delta := newSz - oldSz
|
|
switch {
|
|
case delta > 0:
|
|
return b.Grow(delta)
|
|
case delta < 0:
|
|
b.Shrink(-delta)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResizeTo resizes the budget to the new size
|
|
func (b *Budget) ResizeTo(newSz int64) error {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
if newSz == b.used {
|
|
return nil
|
|
}
|
|
return b.resize(b.used, newSz)
|
|
}
|
|
|
|
// Grow the budget by the given size
|
|
func (b *Budget) Grow(request int64) error {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
if extra := request - b.available(); extra > 0 {
|
|
minExtra := b.pool.roundSize(extra)
|
|
if err := b.pool.allocate(minExtra); err != nil {
|
|
return err
|
|
}
|
|
b.cap += minExtra
|
|
}
|
|
b.used += request
|
|
return nil
|
|
}
|
|
|
|
// Shrink the budget and reduce the given size
|
|
func (b *Budget) Shrink(delta int64) {
|
|
if b == nil || delta == 0 {
|
|
return
|
|
}
|
|
if b.used < delta {
|
|
delta = b.used
|
|
}
|
|
b.used -= delta
|
|
|
|
if b.pool == nil {
|
|
return
|
|
}
|
|
|
|
if release := b.available() - b.pool.allocAlignSize; release > 0 && (b.explicitReserved == 0 || b.used+b.pool.allocAlignSize > b.explicitReserved) {
|
|
b.pool.release(release)
|
|
b.cap -= release
|
|
}
|
|
}
|
|
|
|
func (p *ResourcePool) doAlloc(request int64) error {
|
|
if p.mu.allocated > p.limit-request {
|
|
if p.actions.OutOfLimitActionCB == nil {
|
|
return newBudgetExceededError("out of limit", p, request, p.mu.allocated, p.limit)
|
|
}
|
|
if err := p.actions.OutOfLimitActionCB(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check whether we need to request an increase of our budget.
|
|
if delta := request + p.mu.allocated - p.mu.budget.used - p.reserved; delta > 0 {
|
|
if err := p.increaseBudget(delta); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
p.mu.allocated += request
|
|
if p.mu.maxAllocated < p.mu.allocated {
|
|
p.mu.maxAllocated = p.mu.allocated
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExplicitReserve reserves the budget explicitly
|
|
func (p *ResourcePool) ExplicitReserve(request int64) (err error) {
|
|
p.mu.Lock()
|
|
err = p.mu.budget.Reserve(request)
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
func (p *ResourcePool) allocate(request int64) error {
|
|
{
|
|
p.mu.Lock()
|
|
|
|
if err := p.doAlloc(request); err != nil {
|
|
p.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
if p.actions.NoteAction.CB != nil {
|
|
if allocated := p.allocated(); allocated > p.actions.NoteAction.Threshold {
|
|
p.actions.NoteAction.CB(NoteActionState{
|
|
Pool: p,
|
|
Allocated: allocated,
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ResourcePool) allocated() int64 {
|
|
return atomic.LoadInt64(&p.mu.allocated)
|
|
}
|
|
|
|
func (p *ResourcePool) capacity() int64 {
|
|
return p.mu.budget.cap
|
|
}
|
|
|
|
// ApproxAvailable returns the approximate available budget
|
|
func (p *ResourcePool) ApproxAvailable() int64 {
|
|
return p.mu.budget.available()
|
|
}
|
|
|
|
// ApproxCap returns the approximate capacity of the resource pool
|
|
func (p *ResourcePool) ApproxCap() int64 {
|
|
return p.capacity()
|
|
}
|
|
|
|
// Capacity returns the capacity of the resource pool
|
|
func (p *ResourcePool) Capacity() (res int64) {
|
|
p.mu.Lock()
|
|
res = p.capacity()
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// SetLimit sets the limit of the resource pool
|
|
func (p *ResourcePool) SetLimit(newLimit int64) {
|
|
p.mu.Lock()
|
|
p.limit = newLimit
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *ResourcePool) release(sz int64) {
|
|
p.mu.Lock()
|
|
p.doRelease(sz)
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *ResourcePool) doRelease(sz int64) {
|
|
if p.mu.allocated < sz {
|
|
sz = p.mu.allocated
|
|
}
|
|
p.mu.allocated -= sz
|
|
|
|
p.doAdjustBudget()
|
|
}
|
|
|
|
// SetOutOfCapacityAction sets the out of capacity action
|
|
// It is called when the resource pool is out of capacity
|
|
func (p *ResourcePool) SetOutOfCapacityAction(f func(OutOfCapacityActionArgs) error) {
|
|
p.mu.Lock()
|
|
p.actions.OutOfCapacityActionCB = f
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
// SetOutOfLimitAction sets the out of limit action
|
|
// It is called when the resource pool is out of limit
|
|
func (p *ResourcePool) SetOutOfLimitAction(f func(*ResourcePool) error) {
|
|
p.mu.Lock()
|
|
p.actions.OutOfLimitActionCB = f
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *ResourcePool) increaseBudget(request int64) error {
|
|
if p.mu.budget.pool == nil { // Root Pool
|
|
need := request - p.mu.budget.available()
|
|
if need <= 0 {
|
|
p.mu.budget.used += request
|
|
return nil
|
|
}
|
|
|
|
if p.actions.OutOfCapacityActionCB != nil {
|
|
if err := p.actions.OutOfCapacityActionCB(OutOfCapacityActionArgs{
|
|
Pool: p,
|
|
Request: need,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.mu.budget.used += request
|
|
return nil
|
|
}
|
|
return newBudgetExceededError("out of quota",
|
|
p,
|
|
request,
|
|
p.mu.budget.used,
|
|
p.mu.budget.cap,
|
|
)
|
|
}
|
|
|
|
return p.mu.budget.Grow(request)
|
|
}
|
|
|
|
func (p *ResourcePool) roundSize(sz int64) int64 {
|
|
alignSize := p.allocAlignSize
|
|
if alignSize <= 1 {
|
|
return sz
|
|
}
|
|
return (sz + alignSize - 1) / alignSize * alignSize
|
|
}
|
|
|
|
func (p *ResourcePool) releaseBudget() {
|
|
p.mu.budget.Clear()
|
|
}
|
|
|
|
// AdjustBudget adjusts the budget of the resource pool
|
|
func (p *ResourcePool) AdjustBudget() {
|
|
p.mu.Lock()
|
|
p.doAdjustBudget()
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *ResourcePool) doAdjustBudget() {
|
|
needed := p.mu.allocated - p.reserved
|
|
if needed <= 0 {
|
|
needed = 0
|
|
} else {
|
|
needed = p.roundSize(needed)
|
|
}
|
|
if p.allocAlignSize*p.maxUnusedBlocks <= p.mu.budget.used-needed {
|
|
delta := p.mu.budget.used - needed
|
|
p.mu.budget.Shrink(delta)
|
|
}
|
|
}
|
|
|
|
func (p *ResourcePool) forceAddCap(c int64) {
|
|
if c == 0 {
|
|
return
|
|
}
|
|
p.mu.budget.cap += c
|
|
}
|
|
|
|
func newBudgetExceededError(
|
|
reason string,
|
|
root *ResourcePool,
|
|
requestedBytes int64, allocatedBytes int64, limitBytes int64,
|
|
) error {
|
|
return fmt.Errorf(
|
|
"resource pool `%s` meets `%s`: requested(%d) + allocated(%d) > limit(%d)",
|
|
root.name,
|
|
reason,
|
|
requestedBytes,
|
|
allocatedBytes,
|
|
limitBytes,
|
|
)
|
|
}
|