221 lines
5.8 KiB
Go
221 lines
5.8 KiB
Go
|
package hostpool
|
||
|
|
||
|
import (
|
||
|
"log"
|
||
|
"math/rand"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type epsilonHostPoolResponse struct {
|
||
|
standardHostPoolResponse
|
||
|
started time.Time
|
||
|
ended time.Time
|
||
|
}
|
||
|
|
||
|
func (r *epsilonHostPoolResponse) Mark(err error) {
|
||
|
r.Do(func() {
|
||
|
r.ended = time.Now()
|
||
|
doMark(err, r)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
type epsilonGreedyHostPool struct {
|
||
|
standardHostPool // TODO - would be nifty if we could embed HostPool and Locker interfaces
|
||
|
epsilon float32 // this is our exploration factor
|
||
|
decayDuration time.Duration
|
||
|
EpsilonValueCalculator // embed the epsilonValueCalculator
|
||
|
timer
|
||
|
quit chan bool
|
||
|
}
|
||
|
|
||
|
// Construct an Epsilon Greedy HostPool
|
||
|
//
|
||
|
// Epsilon Greedy is an algorithm that allows HostPool not only to track failure state,
|
||
|
// but also to learn about "better" options in terms of speed, and to pick from available hosts
|
||
|
// based on how well they perform. This gives a weighted request rate to better
|
||
|
// performing hosts, while still distributing requests to all hosts (proportionate to their performance).
|
||
|
// The interface is the same as the standard HostPool, but be sure to mark the HostResponse immediately
|
||
|
// after executing the request to the host, as that will stop the implicitly running request timer.
|
||
|
//
|
||
|
// A good overview of Epsilon Greedy is here http://stevehanov.ca/blog/index.php?id=132
|
||
|
//
|
||
|
// To compute the weighting scores, we perform a weighted average of recent response times, over the course of
|
||
|
// `decayDuration`. decayDuration may be set to 0 to use the default value of 5 minutes
|
||
|
// We then use the supplied EpsilonValueCalculator to calculate a score from that weighted average response time.
|
||
|
func NewEpsilonGreedy(hosts []string, decayDuration time.Duration, calc EpsilonValueCalculator) HostPool {
|
||
|
|
||
|
if decayDuration <= 0 {
|
||
|
decayDuration = defaultDecayDuration
|
||
|
}
|
||
|
stdHP := New(hosts).(*standardHostPool)
|
||
|
p := &epsilonGreedyHostPool{
|
||
|
standardHostPool: *stdHP,
|
||
|
epsilon: float32(initialEpsilon),
|
||
|
decayDuration: decayDuration,
|
||
|
EpsilonValueCalculator: calc,
|
||
|
timer: &realTimer{},
|
||
|
quit: make(chan bool),
|
||
|
}
|
||
|
|
||
|
// allocate structures
|
||
|
for _, h := range p.hostList {
|
||
|
h.epsilonCounts = make([]int64, epsilonBuckets)
|
||
|
h.epsilonValues = make([]int64, epsilonBuckets)
|
||
|
}
|
||
|
go p.epsilonGreedyDecay()
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) Close() {
|
||
|
// No need to do p.quit <- true as close(p.quit) does the trick.
|
||
|
close(p.quit)
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) SetEpsilon(newEpsilon float32) {
|
||
|
p.Lock()
|
||
|
defer p.Unlock()
|
||
|
p.epsilon = newEpsilon
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) SetHosts(hosts []string) {
|
||
|
p.Lock()
|
||
|
defer p.Unlock()
|
||
|
p.standardHostPool.setHosts(hosts)
|
||
|
for _, h := range p.hostList {
|
||
|
h.epsilonCounts = make([]int64, epsilonBuckets)
|
||
|
h.epsilonValues = make([]int64, epsilonBuckets)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) epsilonGreedyDecay() {
|
||
|
durationPerBucket := p.decayDuration / epsilonBuckets
|
||
|
ticker := time.NewTicker(durationPerBucket)
|
||
|
for {
|
||
|
select {
|
||
|
case <-p.quit:
|
||
|
ticker.Stop()
|
||
|
return
|
||
|
case <-ticker.C:
|
||
|
p.performEpsilonGreedyDecay()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
func (p *epsilonGreedyHostPool) performEpsilonGreedyDecay() {
|
||
|
p.Lock()
|
||
|
for _, h := range p.hostList {
|
||
|
h.epsilonIndex += 1
|
||
|
h.epsilonIndex = h.epsilonIndex % epsilonBuckets
|
||
|
h.epsilonCounts[h.epsilonIndex] = 0
|
||
|
h.epsilonValues[h.epsilonIndex] = 0
|
||
|
}
|
||
|
p.Unlock()
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) Get() HostPoolResponse {
|
||
|
p.Lock()
|
||
|
defer p.Unlock()
|
||
|
host := p.getEpsilonGreedy()
|
||
|
if host == "" {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
started := time.Now()
|
||
|
return &epsilonHostPoolResponse{
|
||
|
standardHostPoolResponse: standardHostPoolResponse{host: host, pool: p},
|
||
|
started: started,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) getEpsilonGreedy() string {
|
||
|
var hostToUse *hostEntry
|
||
|
|
||
|
// this is our exploration phase
|
||
|
if rand.Float32() < p.epsilon {
|
||
|
p.epsilon = p.epsilon * epsilonDecay
|
||
|
if p.epsilon < minEpsilon {
|
||
|
p.epsilon = minEpsilon
|
||
|
}
|
||
|
return p.getRoundRobin()
|
||
|
}
|
||
|
|
||
|
// calculate values for each host in the 0..1 range (but not ormalized)
|
||
|
var possibleHosts []*hostEntry
|
||
|
now := time.Now()
|
||
|
var sumValues float64
|
||
|
for _, h := range p.hostList {
|
||
|
if h.canTryHost(now) {
|
||
|
v := h.getWeightedAverageResponseTime()
|
||
|
if v > 0 {
|
||
|
ev := p.CalcValueFromAvgResponseTime(v)
|
||
|
h.epsilonValue = ev
|
||
|
sumValues += ev
|
||
|
possibleHosts = append(possibleHosts, h)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(possibleHosts) != 0 {
|
||
|
// now normalize to the 0..1 range to get a percentage
|
||
|
for _, h := range possibleHosts {
|
||
|
h.epsilonPercentage = h.epsilonValue / sumValues
|
||
|
}
|
||
|
|
||
|
// do a weighted random choice among hosts
|
||
|
ceiling := 0.0
|
||
|
pickPercentage := rand.Float64()
|
||
|
for _, h := range possibleHosts {
|
||
|
ceiling += h.epsilonPercentage
|
||
|
if pickPercentage <= ceiling {
|
||
|
hostToUse = h
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if hostToUse == nil {
|
||
|
if len(possibleHosts) != 0 {
|
||
|
log.Println("Failed to randomly choose a host, Dan loses")
|
||
|
}
|
||
|
|
||
|
return p.getRoundRobin()
|
||
|
}
|
||
|
|
||
|
if hostToUse.dead {
|
||
|
hostToUse.willRetryHost(p.maxRetryInterval)
|
||
|
}
|
||
|
return hostToUse.host
|
||
|
}
|
||
|
|
||
|
func (p *epsilonGreedyHostPool) markSuccess(hostR HostPoolResponse) {
|
||
|
// first do the base markSuccess - a little redundant with host lookup but cleaner than repeating logic
|
||
|
p.standardHostPool.markSuccess(hostR)
|
||
|
eHostR, ok := hostR.(*epsilonHostPoolResponse)
|
||
|
if !ok {
|
||
|
log.Printf("Incorrect type in eps markSuccess!") // TODO reflection to print out offending type
|
||
|
return
|
||
|
}
|
||
|
host := eHostR.host
|
||
|
duration := p.between(eHostR.started, eHostR.ended)
|
||
|
|
||
|
p.Lock()
|
||
|
defer p.Unlock()
|
||
|
h, ok := p.hosts[host]
|
||
|
if !ok {
|
||
|
log.Fatalf("host %s not in HostPool %v", host, p.Hosts())
|
||
|
}
|
||
|
h.epsilonCounts[h.epsilonIndex]++
|
||
|
h.epsilonValues[h.epsilonIndex] += int64(duration.Seconds() * 1000)
|
||
|
}
|
||
|
|
||
|
// --- timer: this just exists for testing
|
||
|
|
||
|
type timer interface {
|
||
|
between(time.Time, time.Time) time.Duration
|
||
|
}
|
||
|
|
||
|
type realTimer struct{}
|
||
|
|
||
|
func (rt *realTimer) between(start time.Time, end time.Time) time.Duration {
|
||
|
return end.Sub(start)
|
||
|
}
|