decred.org/dcrdex@v1.0.5/dex/wait/queue.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package wait 5 6 import ( 7 "context" 8 "math" 9 "sort" 10 "sync" 11 "time" 12 ) 13 14 // TryDirective is a response that a Waiter's TryFunc can return to instruct 15 // the queue to continue trying or to quit. 16 type TryDirective bool 17 18 const ( 19 // TryAgain, when returned from the Waiter's TryFunc, instructs the ticker 20 // queue to try again after the configured delay. 21 TryAgain TryDirective = false 22 // DontTryAgain, when returned from the Waiter's TryFunc, instructs the 23 // ticker queue to quit trying and quit tracking the Waiter. 24 DontTryAgain TryDirective = true 25 ) 26 27 // Waiter is a function to run every recheckInterval until completion or 28 // expiration. Completion is indicated when the TryFunc returns DontTryAgain. 29 // Expiration occurs when TryAgain is returned after Expiration time. 30 type Waiter struct { 31 // Expiration time is checked after the function returns TryAgain. If the 32 // current time > Expiration, ExpireFunc will be run and the waiter will be 33 // un-queued. 34 Expiration time.Time 35 // TryFunc is the function to run periodically until DontTryAgain is 36 // returned or Waiter expires. 37 TryFunc func() TryDirective 38 // ExpireFunc is a function to run in the case that the Waiter expires. 39 ExpireFunc func() 40 41 // Consider: EndFunc that runs after: (1) TryFunc returns DontTryAgain, (2) 42 // ExpireFunc is run, or (3) the queue shuts down. 43 } 44 45 // TickerQueue is a Waiter manager that checks a function periodically until 46 // DontTryAgain is indicated. 47 type TickerQueue struct { 48 waiterMtx sync.RWMutex 49 waiters []*Waiter 50 recheckInterval time.Duration 51 } 52 53 // NewTickerQueue is the constructor for a new TickerQueue. 54 func NewTickerQueue(recheckInterval time.Duration) *TickerQueue { 55 return &TickerQueue{ 56 recheckInterval: recheckInterval, 57 waiters: make([]*Waiter, 0, 256), 58 } 59 } 60 61 // Wait attempts to run the (*Waiter).TryFunc until either 1) the function 62 // returns the value DontTryAgain, or 2) the function's Expiration time has 63 // passed. In the case of 2, the (*Waiter).ExpireFunc will be run. 64 func (q *TickerQueue) Wait(w *Waiter) { 65 if time.Now().After(w.Expiration) { 66 log.Error("wait.TickerQueue: Waiter given expiration before present") 67 return 68 } 69 // Check to see if it passes right away. 70 if w.TryFunc() == DontTryAgain { 71 return 72 } 73 q.waiterMtx.Lock() 74 q.waiters = append(q.waiters, w) 75 q.waiterMtx.Unlock() 76 } 77 78 // Run runs the primary wait loop until the context is canceled. 79 func (q *TickerQueue) Run(ctx context.Context) { 80 // Expire any waiters left on shutdown. 81 defer func() { 82 q.waiterMtx.Lock() 83 for _, w := range q.waiters { 84 w.ExpireFunc() 85 } 86 q.waiters = q.waiters[:0] 87 q.waiterMtx.Unlock() 88 }() 89 // The latencyTicker triggers a check of all waitFunc functions. 90 latencyTicker := time.NewTicker(q.recheckInterval) 91 defer latencyTicker.Stop() 92 93 runWaiters := func() { 94 q.waiterMtx.Lock() 95 defer q.waiterMtx.Unlock() 96 agains := make([]*Waiter, 0) 97 // Grab new waiters 98 tNow := time.Now() 99 for _, w := range q.waiters { 100 if ctx.Err() != nil { 101 return 102 } 103 if w.TryFunc() == DontTryAgain { 104 continue 105 } 106 // If this waiter has expired, issue the timeout error to the client 107 // and do not append to the agains slice. 108 if w.Expiration.Before(tNow) { 109 w.ExpireFunc() 110 continue 111 } 112 agains = append(agains, w) 113 } 114 q.waiters = agains 115 } 116 out: 117 for { 118 select { 119 case <-latencyTicker.C: 120 runWaiters() 121 case <-ctx.Done(): 122 break out 123 } 124 } 125 } 126 127 // tick speed is piecewise linear, constant at fastestInterval at or below 128 // fullSpeedTicks, linear from fastestInterval to slowestInterval between 129 // fullSpeedTicks and fullyTapered, and slowestInterval beyond that. 130 const ( 131 // fullSpeedTicks is the number of attempts that will be made with the 132 // configured fastestInterval delay. After fullSpeedTicks, the retry speed 133 // will be tapered off. 134 fullSpeedTicks = 3 135 // Once the number of attempts has reached fullyTapered, the delay between 136 // attempts will be set to slowestInterval. 137 fullyTapered = 15 138 ) 139 140 type taperingWaiter struct { 141 *Waiter 142 // tick tracks the number of attempts that have been made and is used to 143 // calculate the tapered delay. 144 tick int 145 // nextTick is used to sort the waiters. 146 nextTick time.Time 147 } 148 149 // TaperingTickerQueue is a queue that will run Waiters according to a tapering- 150 // delay schedule. The first attempts will be more frequent, but if they are 151 // not successful, the delay between attempts will grow longer and longer up 152 // to a configurable maximum. 153 type TaperingTickerQueue struct { 154 fastestInterval time.Duration 155 slowestInterval time.Duration 156 queueWaiter chan *taperingWaiter 157 } 158 159 // NewTaperingTickerQueue is a constructor for a TaperingTicketQueue. The 160 // arguments fasterInterval and slowestInterval define how the Waiter attempt 161 // speed is tapered. Initially, attempts will be tried every fastestInterval. 162 // After fullSpeedTicks, the delays will be increased until it reaches 163 // slowestInterval (at fullyTapered). 164 func NewTaperingTickerQueue(fastestInterval, slowestInterval time.Duration) *TaperingTickerQueue { 165 return &TaperingTickerQueue{ 166 fastestInterval: fastestInterval, 167 slowestInterval: slowestInterval, 168 queueWaiter: make(chan *taperingWaiter, 16), 169 } 170 } 171 172 // Wait attempts to run the (*Waiter).TryFunc until either 1) the function 173 // returns the value DontTryAgain, or 2) the function's Expiration time has 174 // passed. In the case of 2, the (*Waiter).ExpireFunc will be run. 175 func (q *TaperingTickerQueue) Wait(waiter *Waiter) { 176 if time.Now().After(waiter.Expiration) { 177 log.Error("wait.TickerQueue: Waiter given expiration before present") 178 return 179 } 180 // We don't want the caller to hang here, so we won't call TryFunc. Instead 181 // set the nextTick as now and the run loop will call it in a goroutine 182 // immediately. 183 q.queueWaiter <- &taperingWaiter{Waiter: waiter, nextTick: time.Now()} 184 } 185 186 // Run runs the primary wait loop until the context is canceled. 187 func (q *TaperingTickerQueue) Run(ctx context.Context) { 188 var wg sync.WaitGroup 189 defer wg.Wait() 190 191 runWaiter := func(w *taperingWaiter) { 192 defer wg.Done() 193 194 if w.TryFunc() == DontTryAgain { 195 return 196 } 197 // If this waiter has expired, issue the timeout error to the client 198 // and don't re-insert. 199 if w.Expiration.Before(time.Now()) { 200 w.ExpireFunc() 201 return 202 } 203 204 w.tick++ 205 w.nextTick = nextTick(w.tick, q.slowestInterval, q.fastestInterval, 206 time.Now(), w.Expiration) 207 208 q.queueWaiter <- w // send it back to the queue 209 } 210 211 waiters := make([]*taperingWaiter, 0, 100) // only used in the loop 212 var timer *time.Timer 213 for { 214 var tick <-chan time.Time 215 if len(waiters) > 0 { 216 if timer != nil { 217 timer.Stop() 218 } 219 timer = time.NewTimer(time.Until(waiters[0].nextTick)) 220 tick = timer.C 221 } 222 223 select { 224 case <-tick: 225 // Remove the next waiter from the slice. runWaiter will re-insert 226 // with a new nextTick time if it sees TryAgain. 227 w := waiters[0] 228 waiters = waiters[1:] 229 wg.Add(1) 230 go runWaiter(w) 231 232 case w := <-q.queueWaiter: 233 // A little optimization if this waiter would fire immediately, but 234 // it works to append regardless. 235 if time.Until(w.nextTick) <= 0 { 236 wg.Add(1) 237 go runWaiter(w) 238 continue 239 } 240 241 waiters = append(waiters, w) 242 sort.Slice(waiters, func(i, j int) bool { 243 return waiters[i].nextTick.Before(waiters[j].nextTick) // ascending, next tick first 244 }) 245 246 case <-ctx.Done(): 247 if timer != nil { 248 timer.Stop() 249 } 250 for _, w := range waiters { 251 w.ExpireFunc() // early, but still ending prior to DontTryAgain 252 } 253 return 254 } 255 } 256 } 257 258 func nextTick(ticksPassed int, slowestInterval, fastestInterval time.Duration, 259 now, expiration time.Time) time.Time { 260 var nextTickTime time.Time 261 switch { 262 case ticksPassed < fullSpeedTicks: 263 nextTickTime = now.Add(fastestInterval) 264 case ticksPassed < fullyTapered: // ramp up the interval 265 prog := float64(ticksPassed+1-fullSpeedTicks) / (fullyTapered - fullSpeedTicks) 266 taper := float64(slowestInterval - fastestInterval) 267 interval := fastestInterval + time.Duration(math.Round(prog*taper)) 268 nextTickTime = now.Add(interval) 269 default: 270 nextTickTime = now.Add(slowestInterval) 271 } 272 273 if nextTickTime.After(expiration) { 274 return expiration 275 } 276 return nextTickTime 277 }