Files
pond/resizer.go
T
2020-05-25 22:50:01 -03:00

163 lines
4.7 KiB
Go

package pond
import (
"container/ring"
"math"
"time"
)
// Preset pool resizing strategies
var (
// Eager maximizes responsiveness at the expense of higher resource usage,
// which can reduce throughput under certain conditions.
// This strategy is meant for worker pools that will operate at a small percentage of their capacity
// most of the time and may occasionally receive bursts of tasks.
Eager = DynamicResizer(1, 0.01)
// Balanced tries to find a balance between responsiveness and throughput.
// It's the default strategy and it's suitable for general purpose worker pools or those
// that will operate close to 50% of their capacity most of the time.
Balanced = DynamicResizer(3, 0.01)
// Lazy maximizes throughput at the expense of responsiveness.
// This strategy is meant for worker pools that will operate close to their max. capacity most of the time.
Lazy = DynamicResizer(5, 0.01)
)
// dynamicResizer implements a configurable dynamic resizing strategy
type dynamicResizer struct {
windowSize int
tolerance float64
incomingTasks *ring.Ring
completedTasks *ring.Ring
duration *ring.Ring
}
// DynamicResizer creates a dynamic resizing strategy that gradually increases or decreases
// the size of the pool to match the rate of incoming tasks (input rate) with the rate of
// completed tasks (output rate).
// windowSize: determines how many cycles to consider when calculating input and output rates.
// tolerance: defines a percentage (between 0 and 1)
func DynamicResizer(windowSize int, tolerance float64) ResizingStrategy {
if windowSize < 1 {
windowSize = 1
}
if tolerance < 0 {
tolerance = 0
}
dynamicResizer := &dynamicResizer{
windowSize: windowSize,
tolerance: tolerance,
}
dynamicResizer.reset()
return dynamicResizer
}
func (r *dynamicResizer) reset() {
// Create rings
r.incomingTasks = ring.New(r.windowSize)
r.completedTasks = ring.New(r.windowSize)
r.duration = ring.New(r.windowSize)
// Initialize with 0s
for i := 0; i < r.windowSize; i++ {
r.incomingTasks.Value = 0
r.completedTasks.Value = 0
r.duration.Value = 0 * time.Second
r.incomingTasks = r.incomingTasks.Next()
r.completedTasks = r.completedTasks.Next()
r.duration = r.duration.Next()
}
}
func (r *dynamicResizer) totalIncomingTasks() int {
var valueSum int = 0
r.incomingTasks.Do(func(value interface{}) {
valueSum += value.(int)
})
return valueSum
}
func (r *dynamicResizer) totalCompletedTasks() int {
var valueSum int = 0
r.completedTasks.Do(func(value interface{}) {
valueSum += value.(int)
})
return valueSum
}
func (r *dynamicResizer) totalDuration() time.Duration {
var valueSum time.Duration = 0
r.duration.Do(func(value interface{}) {
valueSum += value.(time.Duration)
})
return valueSum
}
func (r *dynamicResizer) push(incomingTasks int, completedTasks int, duration time.Duration) {
r.incomingTasks.Value = incomingTasks
r.completedTasks.Value = completedTasks
r.duration.Value = duration
r.incomingTasks = r.incomingTasks.Next()
r.completedTasks = r.completedTasks.Next()
r.duration = r.duration.Next()
}
func (r *dynamicResizer) Resize(runningWorkers, idleWorkers, minWorkers, maxWorkers, incomingTasks, completedTasks int, duration time.Duration) int {
r.push(incomingTasks, completedTasks, duration)
windowIncomingTasks := r.totalIncomingTasks()
windowCompletedTasks := r.totalCompletedTasks()
windowSecs := r.totalDuration().Seconds()
windowInputRate := float64(windowIncomingTasks) / windowSecs
windowOutputRate := float64(windowCompletedTasks) / windowSecs
if runningWorkers == 0 || windowCompletedTasks == 0 {
// No workers yet, create as many workers ar.incomingTasks-idleWorkers
delta := incomingTasks - idleWorkers
return r.fitDelta(delta, runningWorkers, minWorkers, maxWorkers)
}
deltaRate := windowInputRate - windowOutputRate
// No changes, do not resize
if deltaRate == 0 {
return 0
}
// If delta % is below the defined tolerance, do not resize
if r.tolerance > 0 {
deltaPercentage := math.Abs(deltaRate / windowInputRate)
if deltaPercentage < r.tolerance {
return 0
}
}
if deltaRate > 0 {
// Need to grow the pool
workerRate := windowOutputRate / float64(runningWorkers)
ratio := windowSecs / float64(r.windowSize)
delta := int(ratio*(deltaRate/workerRate)) - idleWorkers
if deltaRate > 0 && delta < 1 {
delta = 1
}
return r.fitDelta(delta, runningWorkers, minWorkers, maxWorkers)
} else if deltaRate < 0 && idleWorkers > 0 {
// Need to shrink the pool
return r.fitDelta(-1, runningWorkers, minWorkers, maxWorkers)
}
return 0
}
func (r *dynamicResizer) fitDelta(delta, current, min, max int) int {
if current+delta < min {
delta = -(current - min)
}
if current+delta > max {
delta = max - current
}
return delta
}