package pool import ( "code.squareroundforest.org/arpio/times" "math" "time" ) const ( // arbitrary values to be most likely out of sync with anything else: minNightshiftTime = 729 * time.Millisecond // ~1sec 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 } func (a *adaptive) nightshift(s Stats) time.Duration { pactive := a.prevState.Active > 0 empty := s.Idle == 0 active := s.Active > 0 a.prevState = s // on the first put we need to set an active start time: if !a.initialized { a.initialized = true a.activeStart = a.clock.Now() if active { return 0 } return a.nightshiftTO() } if !pactive && !active && !empty { return a.nightshiftTO() } if !pactive && active { a.activeStart = a.clock.Now() return 0 } if pactive && !active { a.activeEnd = a.clock.Now() if empty { return 0 } return a.nightshiftTO() } return 0 } func (a *adaptive) Target(s Stats) (int, time.Duration) { t := a.target(s) // 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.initialized || a.activeStart.After(a.activeEnd)) { t = 2 } ns := a.nightshift(s) 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) }