256 lines
7.6 KiB
Go
256 lines
7.6 KiB
Go
package pool
|
|
|
|
import (
|
|
"code.squareroundforest.org/arpio/times"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// arbitrary values to be most likely out of sync with anything else:
|
|
minNightshiftTime = 6561 * time.Millisecond // ~6sec
|
|
maxNightshiftTime = 59049 * time.Second // ~2/3day
|
|
)
|
|
|
|
type adaptive struct {
|
|
clock times.Clock
|
|
prevState Stats
|
|
initialized bool
|
|
activeStart time.Time
|
|
activeEnd time.Time
|
|
average float64
|
|
deviation float64
|
|
}
|
|
|
|
func makeAdaptiveAlgo() *adaptive {
|
|
return &adaptive{}
|
|
}
|
|
|
|
func (a *adaptive) setClock(c times.Clock) {
|
|
a.clock = c
|
|
}
|
|
|
|
func movingAverage(prev, currv float64) float64 {
|
|
return prev + (currv-prev)/math.E
|
|
}
|
|
|
|
func movingAbsoluteDeviation(prev, currv, currav float64) float64 {
|
|
return prev + (math.Abs(currv-currav)-prev)/math.E
|
|
}
|
|
|
|
func targetCapacity(av, dev float64) float64 {
|
|
return av + dev*math.E
|
|
}
|
|
|
|
func (a *adaptive) target(s Stats) int {
|
|
av := movingAverage(a.average, float64(s.Active))
|
|
dev := movingAbsoluteDeviation(a.deviation, float64(s.Active), av)
|
|
a.average = av
|
|
a.deviation = dev
|
|
return int(targetCapacity(av, dev))
|
|
}
|
|
|
|
func (a *adaptive) nightshiftTO() time.Duration {
|
|
to := a.activeEnd.Sub(a.activeStart)
|
|
to = (3 * to) / 8
|
|
if to < minNightshiftTime {
|
|
to = minNightshiftTime
|
|
}
|
|
|
|
if to > maxNightshiftTime {
|
|
to = maxNightshiftTime
|
|
}
|
|
|
|
return to
|
|
}
|
|
|
|
// ensure that a single client does not get freed if using it
|
|
// ensure that eventually gets collected if not using it
|
|
// ensure that the background job is not running forever
|
|
// handle load as well
|
|
|
|
func (a *adaptive) nightshift(s Stats, now time.Time) time.Duration {
|
|
// idle, inactive, uninitialized => not idle, initialize, 0
|
|
// idle, inactive, initialized => to
|
|
// idle, active, uninitialized => not idle, initialize, 0
|
|
// idle, active, initialized => not idle, initialize, 0
|
|
// not idle, inactive, uninitialized => not idle, initialize, 0
|
|
// not idle, inactive, initialized => idle, new to
|
|
// not idle, active, uninitialized => not idle, initialize, 0
|
|
// not idle, active, initialized => 0
|
|
|
|
// idle := a.idle
|
|
// active := s.Active > 0
|
|
// initialized := !a.activeTime.IsZero()
|
|
// if !initialized {
|
|
// a.idle = false
|
|
// a.activeTime = now
|
|
// return 0
|
|
// }
|
|
|
|
// if idle && !active {
|
|
// return a.nsTO
|
|
// }
|
|
|
|
// if idle {
|
|
// a.idle = false
|
|
// a.nsTO = 0
|
|
// a.activeTime = now
|
|
// return 0
|
|
// }
|
|
|
|
// if active {
|
|
// return 0
|
|
// }
|
|
|
|
// a.idle = true
|
|
// a.nsTO = nightshiftTO(a.activeTime, now)
|
|
// return a.nsTO
|
|
|
|
// --
|
|
|
|
// state flags:
|
|
// - initialized
|
|
// - pempty
|
|
// - pactive
|
|
// - empty
|
|
// - active
|
|
// actions:
|
|
// - start active
|
|
// - call to
|
|
// - update prev state on every call
|
|
// states:
|
|
// X not initialized, not pempty, not pactive, not empty, not active
|
|
// X not initialized, not pempty, not pactive, not empty, active
|
|
// X not initialized, not pempty, not pactive, empty, not active
|
|
// X not initialized, not pempty, not pactive, empty, active
|
|
// X not initialized, not pempty, pactive, not empty, not active
|
|
// X not initialized, not pempty, pactive, not empty, active
|
|
// X not initialized, not pempty, pactive, empty, not active
|
|
// X not initialized, not pempty, pactive, empty, active
|
|
// * not initialized, pempty, not pactive, not empty, not active => start active, call to
|
|
// * not initialized, pempty, not pactive, not empty, active => start active
|
|
// X not initialized, pempty, not pactive, empty, not active
|
|
// X not initialized, pempty, not pactive, empty, active
|
|
// X not initialized, pempty, pactive, not empty, not active
|
|
// X not initialized, pempty, pactive, not empty, active
|
|
// X not initialized, pempty, pactive, empty, not active
|
|
// X not initialized, pempty, pactive, empty, active
|
|
// * initialized, not pempty, not pactive, not empty, not active => end active, call to
|
|
// * initialized, not pempty, not pactive, not empty, active => start active
|
|
// * initialized, not pempty, not pactive, empty, not active => noop
|
|
// * initialized, not pempty, not pactive, empty, active => start active
|
|
// * initialized, not pempty, pactive, not empty, not active => end active, call to
|
|
// * initialized, not pempty, pactive, not empty, active => noop
|
|
// * initialized, not pempty, pactive, empty, not active => end active, noop
|
|
// * initialized, not pempty, pactive, empty, active => noop
|
|
// * initialized, pempty, not pactive, not empty, not active => call to
|
|
// * initialized, pempty, not pactive, not empty, active => start active
|
|
// * initialized, pempty, not pactive, empty, not active => noop
|
|
// * initialized, pempty, not pactive, empty, active => start active
|
|
// * initialized, pempty, pactive, not empty, not active => end active, call to
|
|
// * initialized, pempty, pactive, not empty, active => noop
|
|
// - initialized, pempty, pactive, empty, not active => end active, noop
|
|
// * initialized, pempty, pactive, empty, active => noop
|
|
|
|
pempty := a.prevState.Idle == 0
|
|
pactive := a.prevState.Active > 0
|
|
empty := s.Idle == 0
|
|
active := s.Active > 0
|
|
a.prevState = s
|
|
|
|
// not initialized, pempty, not pactive, not empty, not active => start active, call to
|
|
if !a.initialized && pempty && !pactive && !empty && !active {
|
|
a.initialized = true
|
|
a.activeStart = now
|
|
return a.nightshiftTO()
|
|
}
|
|
|
|
// not initialized, pempty, not pactive, not empty, active => start active
|
|
if !a.initialized && pempty && !pactive && !empty && active {
|
|
a.initialized = true
|
|
a.activeStart = now
|
|
return 0
|
|
}
|
|
|
|
if !a.initialized {
|
|
return 0
|
|
}
|
|
|
|
// initialized, not pempty, not pactive, not empty, not active => end active, call to
|
|
if !pempty && !pactive && !empty && !active {
|
|
ns := a.nightshiftTO()
|
|
return ns
|
|
}
|
|
|
|
// initialized, not pempty, not pactive, not empty, active => start active
|
|
// initialized, not pempty, not pactive, empty, active => start active
|
|
if !pempty && !pactive && active {
|
|
a.activeStart = now
|
|
return 0
|
|
}
|
|
|
|
// initialized, not pempty, pactive, not empty, not active => end active, call to
|
|
if !pempty && pactive && !empty && !active {
|
|
a.activeEnd = now
|
|
ns := a.nightshiftTO()
|
|
return ns
|
|
}
|
|
|
|
// initialized, not pempty, pactive, empty, not active => end active, noop
|
|
if !pempty && pactive && empty && !active {
|
|
a.activeEnd = now
|
|
return 0
|
|
}
|
|
|
|
// initialized, pempty, not pactive, not empty, not active => call to
|
|
if pempty && !pactive && !empty && !active {
|
|
return a.nightshiftTO()
|
|
}
|
|
|
|
// initialized, pempty, not pactive, not empty, active => start active
|
|
// initialized, pempty, not pactive, empty, active => start active
|
|
if pempty && !pactive && active {
|
|
a.activeStart = now
|
|
return 0
|
|
}
|
|
|
|
// initialized, pempty, pactive, not empty, not active => end active, call to
|
|
if pempty && pactive && !empty && !active {
|
|
a.activeEnd = now
|
|
return a.nightshiftTO()
|
|
}
|
|
|
|
// initialized, pempty, pactive, empty, not active => end active, noop
|
|
if pempty && pactive && empty && !active {
|
|
a.activeEnd = now
|
|
return 0
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (a *adaptive) Target(s Stats) (int, time.Duration) {
|
|
t := a.target(s) // handle when t < 2
|
|
|
|
// 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
|
|
}
|
|
|
|
// TODO: optimize, only take the clock when necessary
|
|
ns := a.nightshift(s, a.clock.Now())
|
|
|
|
return t, ns
|
|
}
|
|
|
|
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)
|
|
s := a.prevState
|
|
s.Idle += n
|
|
a.nightshift(s, a.clock.Now())
|
|
}
|