epsilon_greedy.go 4.6 KB

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