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()) }