2026-03-04 15:03:29 +01:00
|
|
|
package pool
|
|
|
|
|
|
2026-03-05 12:34:35 +01:00
|
|
|
import (
|
|
|
|
|
"code.squareroundforest.org/arpio/times"
|
2026-03-14 19:03:18 +01:00
|
|
|
"math"
|
2026-03-05 12:34:35 +01:00
|
|
|
"time"
|
|
|
|
|
)
|
2026-03-04 15:03:29 +01:00
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// arbitrary values to be most likely out of sync with anything else:
|
2026-03-15 17:56:00 +01:00
|
|
|
minNightshiftTime = 729 * time.Millisecond // ~1sec
|
|
|
|
|
maxNightshiftTime = 59049 * time.Second // ~2/3day
|
2026-03-04 15:03:29 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type adaptive struct {
|
2026-03-15 17:18:20 +01:00
|
|
|
clock times.Clock
|
|
|
|
|
prevState Stats
|
|
|
|
|
initialized bool
|
|
|
|
|
activeStart time.Time
|
|
|
|
|
activeEnd time.Time
|
|
|
|
|
average float64
|
|
|
|
|
deviation float64
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeAdaptiveAlgo() *adaptive {
|
2026-03-15 17:18:20 +01:00
|
|
|
return &adaptive{}
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:34:35 +01:00
|
|
|
func (a *adaptive) setClock(c times.Clock) {
|
|
|
|
|
a.clock = c
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 19:03:18 +01:00
|
|
|
func movingAverage(prev, currv float64) float64 {
|
|
|
|
|
return prev + (currv-prev)/math.E
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 19:03:18 +01:00
|
|
|
func movingAbsoluteDeviation(prev, currv, currav float64) float64 {
|
|
|
|
|
return prev + (math.Abs(currv-currav)-prev)/math.E
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 19:03:18 +01:00
|
|
|
func targetCapacity(av, dev float64) float64 {
|
|
|
|
|
return av + dev*math.E
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *adaptive) target(s Stats) int {
|
2026-03-14 19:03:18 +01:00
|
|
|
av := movingAverage(a.average, float64(s.Active))
|
|
|
|
|
dev := movingAbsoluteDeviation(a.deviation, float64(s.Active), av)
|
2026-03-04 15:03:29 +01:00
|
|
|
a.average = av
|
|
|
|
|
a.deviation = dev
|
2026-03-14 19:03:18 +01:00
|
|
|
return int(targetCapacity(av, dev))
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:18:20 +01:00
|
|
|
func (a *adaptive) nightshiftTO() time.Duration {
|
|
|
|
|
to := a.activeEnd.Sub(a.activeStart)
|
|
|
|
|
to = (3 * to) / 8
|
|
|
|
|
if to < minNightshiftTime {
|
|
|
|
|
to = minNightshiftTime
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:18:20 +01:00
|
|
|
if to > maxNightshiftTime {
|
|
|
|
|
to = maxNightshiftTime
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return to
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
func (a *adaptive) nightshift(s Stats) time.Duration {
|
2026-03-15 17:18:20 +01:00
|
|
|
pactive := a.prevState.Active > 0
|
|
|
|
|
empty := s.Idle == 0
|
|
|
|
|
active := s.Active > 0
|
|
|
|
|
a.prevState = s
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
// on the first put we need to set an active start time:
|
2026-03-15 17:18:20 +01:00
|
|
|
if !a.initialized {
|
2026-03-15 17:44:03 +01:00
|
|
|
a.initialized = true
|
|
|
|
|
a.activeStart = a.clock.Now()
|
|
|
|
|
if active {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
2026-03-04 15:03:29 +01:00
|
|
|
|
2026-03-15 17:18:20 +01:00
|
|
|
return a.nightshiftTO()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
if !pactive && !active && !empty {
|
2026-03-15 17:18:20 +01:00
|
|
|
return a.nightshiftTO()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
if !pactive && active {
|
|
|
|
|
a.activeStart = a.clock.Now()
|
2026-03-15 17:18:20 +01:00
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
if pactive && !active {
|
|
|
|
|
a.activeEnd = a.clock.Now()
|
|
|
|
|
if empty {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return a.nightshiftTO()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:18:20 +01:00
|
|
|
return 0
|
2026-03-04 15:03:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *adaptive) Target(s Stats) (int, time.Duration) {
|
2026-03-15 17:44:03 +01:00
|
|
|
t := a.target(s)
|
2026-03-15 17:18:20 +01:00
|
|
|
|
|
|
|
|
// magic number 2: we allow max 2 idle items to be collected only by the nightshift, to provide better
|
|
|
|
|
// support for sporadic requests, when it's active or just going inactive:
|
|
|
|
|
if t < 2 && a.activeStart.After(a.activeEnd) {
|
|
|
|
|
t = 2
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:44:03 +01:00
|
|
|
ns := a.nightshift(s)
|
2026-03-04 15:03:29 +01:00
|
|
|
return t, ns
|
|
|
|
|
}
|
2026-03-14 19:03:18 +01:00
|
|
|
|
|
|
|
|
func (a *adaptive) Load(n int) {
|
|
|
|
|
// we lie to the algorithm when adding the additional idle count to the average active count. This way
|
|
|
|
|
// we can adjust the calculated target capacity:
|
|
|
|
|
a.average += float64(n)
|
2026-03-15 17:18:20 +01:00
|
|
|
s := a.prevState
|
|
|
|
|
s.Idle += n
|
2026-03-15 17:44:03 +01:00
|
|
|
a.nightshift(s)
|
2026-03-14 19:03:18 +01:00
|
|
|
}
|