epsilon_greedy.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package hostpool
  2. import (
  3. "log"
  4. "math/rand"
  5. "time"
  6. )
  7. type epsilonGreedyHostEntry interface {
  8. HostEntry
  9. EpsilonDecayStore
  10. }
  11. type epsilonGreedyHostPool struct {
  12. HostPool
  13. hosts map[string]epsilonGreedyHostEntry // this basically just mirrors the underlying host map
  14. epsilon float32 // this is our exploration factor
  15. EpsilonValueCalculator // embed the epsilonValueCalculator
  16. timer
  17. }
  18. type epsilonHostPoolResponse struct {
  19. standardHostPoolResponse
  20. started time.Time
  21. ended time.Time
  22. }
  23. // ------ constants -------------------
  24. const epsilonDecay = 0.90 // decay the exploration rate
  25. const minEpsilon = 0.01 // explore one percent of the time
  26. const initialEpsilon = 0.3
  27. func toEGHostEntry(fromHE HostEntry) epsilonGreedyHostEntry {
  28. return &struct {
  29. HostEntry
  30. EpsilonDecayStore
  31. }{
  32. fromHE,
  33. NewDecayStore(),
  34. }
  35. }
  36. // Epsilon Greedy is an algorithim that allows HostPool not only to track failure state,
  37. // but also to learn about "better" options in terms of speed, and to pick from available hosts
  38. // based on a percentage of how well they perform. This gives a weighted request rate to better
  39. // performing hosts, while still distributing requests to all hosts (proportionate to their performance)
  40. //
  41. // After enabling Epsilon Greedy, hosts must be marked for sucess along with a time value representing
  42. // how fast (or slow) that host was.
  43. //
  44. // host := pool.Get()
  45. // start := time.Now()
  46. // ..... do work with host
  47. // duration = time.Now().Sub(start)
  48. // pool.MarkSuccessWithTime(host, duration)
  49. //
  50. // a good overview of Epsilon Greedy is here http://stevehanov.ca/blog/index.php?id=132
  51. //
  52. // decayDuration may be set to 0 to use the default value of 5 minutes
  53. func NewEpsilonGreedy(hosts []string, decayDuration time.Duration, calc EpsilonValueCalculator) HostPool {
  54. return ToEpsilonGreedy(New(hosts), decayDuration, calc)
  55. }
  56. func ToEpsilonGreedy(pool HostPool, decayDuration time.Duration, calc EpsilonValueCalculator) HostPool {
  57. if decayDuration <= 0 {
  58. decayDuration = defaultDecayDuration
  59. }
  60. p := &epsilonGreedyHostPool{
  61. HostPool: pool,
  62. epsilon: float32(initialEpsilon),
  63. EpsilonValueCalculator: calc,
  64. timer: &realTimer{},
  65. }
  66. p.hosts = make(map[string]epsilonGreedyHostEntry)
  67. for _, hostName := range pool.Hosts() {
  68. p.hosts[hostName] = toEGHostEntry(pool.lookupHost(hostName))
  69. }
  70. return p
  71. }
  72. // ---------
  73. func (p *epsilonGreedyHostPool) getEpsilonGreedy() string {
  74. // this is our exploration phase
  75. if rand.Float32() < p.epsilon {
  76. p.epsilon = p.epsilon * epsilonDecay
  77. if p.epsilon < minEpsilon {
  78. p.epsilon = minEpsilon
  79. }
  80. return p.HostPool.Get().Host()
  81. }
  82. // calculate values for each host in the 0..1 range (but not ormalized)
  83. type epsilonChoice struct {
  84. he HostEntry
  85. epsilonValue float64
  86. epsilonPercentage float64
  87. }
  88. var hostToUse *epsilonChoice
  89. var possibleHosts []*epsilonChoice
  90. now := time.Now()
  91. var sumValues float64
  92. for _, h := range p.hosts {
  93. ec := &epsilonChoice{he: h}
  94. if h.canTryHost(now) {
  95. v := h.GetWeightedAvgScore() // score is the response time
  96. if v > 0 {
  97. ev := p.CalcValueFromAvgResponseTime(v)
  98. ec.epsilonValue = ev
  99. sumValues += ev
  100. possibleHosts = append(possibleHosts, ec)
  101. }
  102. }
  103. }
  104. if len(possibleHosts) != 0 {
  105. // now normalize to the 0..1 range to get a percentage
  106. for _, ec := range possibleHosts {
  107. ec.epsilonPercentage = ec.epsilonValue / sumValues
  108. }
  109. // do a weighted random choice among hosts
  110. ceiling := 0.0
  111. pickPercentage := rand.Float64()
  112. for _, ec := range possibleHosts {
  113. ceiling += ec.epsilonPercentage
  114. if pickPercentage <= ceiling {
  115. hostToUse = ec
  116. break
  117. }
  118. }
  119. }
  120. if hostToUse == nil {
  121. if len(possibleHosts) != 0 {
  122. log.Println("Failed to randomly choose a host, Dan loses")
  123. }
  124. return p.HostPool.Get().Host()
  125. }
  126. if hostToUse.he.IsDead() {
  127. hostToUse.he.willRetryHost()
  128. }
  129. return hostToUse.he.Host()
  130. }
  131. func (p *epsilonGreedyHostPool) markSuccess(hostR HostPoolResponse) {
  132. // first do the base markSuccess - a little redundant with host lookup but cleaner than repeating logic
  133. p.HostPool.markSuccess(hostR)
  134. eHostR, ok := hostR.(*epsilonHostPoolResponse)
  135. if !ok {
  136. log.Printf("Incorrect type in eps markSuccess!") // TODO reflection to print out offending type
  137. return
  138. }
  139. host := eHostR.host
  140. duration := p.between(eHostR.started, eHostR.ended)
  141. h := p.hosts[host]
  142. h.Record(duration.Seconds())
  143. }