Files
core/process/limits.go
2023-04-25 15:55:32 +02:00

260 lines
5.1 KiB
Go

package process
import (
"context"
"fmt"
"sync"
"time"
"github.com/datarhei/core/v16/psutil"
)
type Usage struct {
CPU struct {
Current float64 // percent 0-100
Average float64 // percent 0-100
Max float64 // percent 0-100
Limit float64 // percent 0-100
}
Memory struct {
Current uint64 // bytes
Average float64 // bytes
Max uint64 // bytes
Limit uint64 // bytes
}
}
type LimitFunc func(cpu float64, memory uint64)
type LimiterConfig struct {
CPU float64 // Max. CPU usage in percent
Memory uint64 // Max. memory usage in bytes
WaitFor time.Duration // Duration one of the limits has to be above the limit until OnLimit gets triggered
OnLimit LimitFunc // Function to be triggered if limits are exceeded
}
type Limiter interface {
// Start starts the limiter with a psutil.Process.
Start(process psutil.Process) error
// Stop stops the limiter. The limiter can be reused by calling Start() again
Stop()
// Current returns the current CPU and memory values
Current() (cpu float64, memory uint64)
// Limits returns the defined CPU and memory limits. Values <= 0 means no limit
Limits() (cpu float64, memory uint64)
// Usage returns the current state of the limiter, such as current, average, max, and
// limit values for CPU and memory.
Usage() Usage
}
type limiter struct {
proc psutil.Process
lock sync.Mutex
cancel context.CancelFunc
onLimit LimitFunc
cpu float64
cpuCurrent float64
cpuMax float64
cpuAvg float64
cpuAvgCounter uint64
cpuLast float64
cpuLimitSince time.Time
memory uint64
memoryCurrent uint64
memoryMax uint64
memoryAvg float64
memoryAvgCounter uint64
memoryLast uint64
memoryLimitSince time.Time
waitFor time.Duration
}
// NewLimiter returns a new Limiter
func NewLimiter(config LimiterConfig) Limiter {
l := &limiter{
cpu: config.CPU,
memory: config.Memory,
waitFor: config.WaitFor,
onLimit: config.OnLimit,
}
if l.onLimit == nil {
l.onLimit = func(float64, uint64) {}
}
return l
}
func (l *limiter) reset() {
l.cpuCurrent = 0
l.cpuLast = 0
l.cpuAvg = 0
l.cpuAvgCounter = 0
l.cpuMax = 0
l.memoryCurrent = 0
l.memoryLast = 0
l.memoryAvg = 0
l.memoryAvgCounter = 0
l.memoryMax = 0
}
func (l *limiter) Start(process psutil.Process) error {
l.lock.Lock()
defer l.lock.Unlock()
if l.proc != nil {
return fmt.Errorf("limiter is already running")
}
l.reset()
l.proc = process
ctx, cancel := context.WithCancel(context.Background())
l.cancel = cancel
go l.ticker(ctx, 500*time.Millisecond)
return nil
}
func (l *limiter) Stop() {
l.lock.Lock()
defer l.lock.Unlock()
if l.proc == nil {
return
}
l.cancel()
l.proc.Stop()
l.proc = nil
l.reset()
}
func (l *limiter) ticker(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case t := <-ticker.C:
l.collect(t)
}
}
}
func (l *limiter) collect(t time.Time) {
l.lock.Lock()
defer l.lock.Unlock()
if l.proc == nil {
return
}
if mstat, err := l.proc.VirtualMemory(); err == nil {
l.memoryLast, l.memoryCurrent = l.memoryCurrent, mstat
if l.memoryCurrent > l.memoryMax {
l.memoryMax = l.memoryCurrent
}
l.memoryAvgCounter++
l.memoryAvg = ((l.memoryAvg * float64(l.memoryAvgCounter-1)) + float64(l.memoryCurrent)) / float64(l.memoryAvgCounter)
}
if cpustat, err := l.proc.CPUPercent(); err == nil {
l.cpuLast, l.cpuCurrent = l.cpuCurrent, cpustat.System+cpustat.User+cpustat.Other
if l.cpuCurrent > l.cpuMax {
l.cpuMax = l.cpuCurrent
}
l.cpuAvgCounter++
l.cpuAvg = ((l.cpuAvg * float64(l.cpuAvgCounter-1)) + l.cpuCurrent) / float64(l.cpuAvgCounter)
}
isLimitExceeded := false
if l.cpu > 0 {
if l.cpuCurrent > l.cpu {
// Current value is higher than the limit
if l.cpuLast <= l.cpu {
// If the previous value is below the limit, then we reached the
// limit as of now
l.cpuLimitSince = time.Now()
}
if time.Since(l.cpuLimitSince) >= l.waitFor {
isLimitExceeded = true
}
}
}
if l.memory > 0 {
if l.memoryCurrent > l.memory {
// Current value is higher than the limit
if l.memoryLast <= l.memory {
// If the previous value is below the limit, then we reached the
// limit as of now
l.memoryLimitSince = time.Now()
}
if time.Since(l.memoryLimitSince) >= l.waitFor {
isLimitExceeded = true
}
}
}
if isLimitExceeded {
go l.onLimit(l.cpuCurrent, l.memoryCurrent)
}
}
func (l *limiter) Current() (cpu float64, memory uint64) {
l.lock.Lock()
defer l.lock.Unlock()
cpu = l.cpuCurrent
memory = l.memoryCurrent
return
}
func (l *limiter) Usage() Usage {
l.lock.Lock()
defer l.lock.Unlock()
usage := Usage{}
usage.CPU.Limit = l.cpu
usage.CPU.Current = l.cpuCurrent
usage.CPU.Average = l.cpuAvg
usage.CPU.Max = l.cpuMax
usage.Memory.Limit = l.memory
usage.Memory.Current = l.memoryCurrent
usage.Memory.Average = l.memoryAvg
usage.Memory.Max = l.memoryMax
return usage
}
func (l *limiter) Limits() (cpu float64, memory uint64) {
return l.cpu, l.memory
}