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  }