decred.org/dcrdex@v1.0.5/dex/wait/queue_test.go (about)

     1  package wait
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  )
    10  
    11  func TestTaperingQueueExpiration(t *testing.T) {
    12  	ctx, cancel := context.WithCancel(context.Background())
    13  	defer cancel()
    14  
    15  	q := NewTaperingTickerQueue(time.Millisecond, time.Millisecond*10)
    16  	go q.Run(ctx)
    17  
    18  	var expirationTime time.Time
    19  	var expiration sync.WaitGroup
    20  	expiration.Add(1)
    21  
    22  	wantExpirationTime := time.Now().Add(time.Millisecond * 10)
    23  	q.Wait(&Waiter{
    24  		Expiration: wantExpirationTime,
    25  		TryFunc: func() TryDirective {
    26  			return TryAgain
    27  		},
    28  		ExpireFunc: func() {
    29  			expirationTime = time.Now()
    30  			expiration.Done()
    31  		},
    32  	})
    33  
    34  	expiration.Wait()
    35  
    36  	if expirationTime.Before(wantExpirationTime) {
    37  		t.Fatalf("expired at: %v - sooner than expected: %v", expirationTime, wantExpirationTime)
    38  	}
    39  }
    40  
    41  func TestTaperingQueue(t *testing.T) {
    42  	const fastestInterval, slowestInterval = 1 * time.Millisecond, 2 * time.Millisecond
    43  	expiration := time.Now().Add(time.Minute)
    44  
    45  	q := NewTaperingTickerQueue(fastestInterval, slowestInterval)
    46  
    47  	// waiterTriesTimedMtx protects waiterTriesTimed from concurrent access.
    48  	var waiterTriesTimedMtx sync.Mutex
    49  	// waiterTriesTimed maps waiter to a list of tries, each try is represented
    50  	// by timestamp (that reflects when waiter try starts executing).
    51  	waiterTriesTimed := make(map[int][]time.Time, 5)
    52  	var wgWaiters sync.WaitGroup
    53  	addWaiter := func(waiterNumber, numTryAgains int) {
    54  		var numTrys int
    55  		q.Wait(&Waiter{
    56  			Expiration: expiration,
    57  			TryFunc: func() TryDirective {
    58  				waiterTriesTimedMtx.Lock()
    59  				// Record when try func was called to check it later.
    60  				waiterTriesTimed[waiterNumber] = append(waiterTriesTimed[waiterNumber], time.Now())
    61  				waiterTriesTimedMtx.Unlock()
    62  				numTrys++
    63  				if numTrys > numTryAgains {
    64  					wgWaiters.Done()
    65  					return DontTryAgain
    66  				}
    67  				return TryAgain
    68  			},
    69  			// We don't expect expire func being called in this test, leaving it
    70  			// undefined so that we'll get a panic in case it gets called.
    71  			//ExpireFunc: func() {},
    72  		})
    73  	}
    74  
    75  	wgWaiters.Add(5)
    76  	addWaiter(0, 20)
    77  	addWaiter(1, 0)
    78  	addWaiter(2, 10)
    79  	addWaiter(3, 3)
    80  	addWaiter(4, 1)
    81  
    82  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    83  	defer cancel()
    84  	var wgQ sync.WaitGroup
    85  	wgQ.Add(1)
    86  	go func() {
    87  		defer wgQ.Done()
    88  		q.Run(ctx)
    89  	}()
    90  
    91  	// Wait for each waiter to get done, then stop the ticker queue itself and
    92  	// wait for it.
    93  	wgWaiters.Wait()
    94  	cancel()
    95  	wgQ.Wait()
    96  
    97  	calcExpectedTicks := func(totalTicks int) []time.Time {
    98  		return expTickSchedule(time.Now(), fastestInterval, slowestInterval)[:totalTicks]
    99  	}
   100  	// There is always at least one tick per waiter we expect (hence +1).
   101  	expWaiterTriesTimes := map[int][]time.Time{
   102  		0: calcExpectedTicks(20 + 1),
   103  		1: calcExpectedTicks(0 + 1),
   104  		2: calcExpectedTicks(10 + 1),
   105  		3: calcExpectedTicks(3 + 1),
   106  		4: calcExpectedTicks(1 + 1),
   107  	}
   108  	for waiterNumber, wantTriesTimes := range expWaiterTriesTimes {
   109  		gotTriesTimed := waiterTriesTimed[waiterNumber]
   110  		if len(gotTriesTimed) != len(wantTriesTimes) {
   111  			t.Fatalf("expected waiter %d to execute %d tries, got %d instead",
   112  				waiterNumber, len(wantTriesTimes), len(gotTriesTimed))
   113  		}
   114  		var (
   115  			prevWantTryTime time.Time
   116  			prevGotTryTimed time.Time
   117  		)
   118  		for i, wantTryTime := range wantTriesTimes {
   119  			gotTryTimed := gotTriesTimed[i]
   120  			if i == 0 {
   121  				// Can't compare try difference for first try since there is nothing
   122  				// to compare against.
   123  				prevWantTryTime = wantTryTime
   124  				prevGotTryTimed = gotTryTimed
   125  				continue
   126  			}
   127  			// Check that waiter tapering works, in other words each waiter try attempt
   128  			// doesn't execute sooner than we expect.
   129  			// We compare the actual observed time difference between two adjacent waiter
   130  			// try-attempts with synthetically calculated one (in wantTryDiff var), the
   131  			// actual observed should be higher-or-equal because there is additional
   132  			// code executing (scheduling/executing retry-attempts and such).
   133  			wantTryDiff := wantTryTime.Sub(prevWantTryTime)
   134  			gotTryDiff := gotTryTimed.Sub(prevGotTryTimed)
   135  			if gotTryDiff < wantTryDiff {
   136  				t.Fatalf("expected waiter %d to have time difference between tries %d-%d be > %v, "+
   137  					"got time difference: %v", waiterNumber, i, i-1, wantTryDiff, gotTryDiff)
   138  			}
   139  			prevWantTryTime = wantTryTime
   140  			prevGotTryTimed = gotTryTimed
   141  		}
   142  	}
   143  }
   144  
   145  func Test_nextTick(t *testing.T) {
   146  	const fastestInterval, slowestInterval = 100 * time.Millisecond, 500 * time.Millisecond
   147  	var gotTicks []time.Time
   148  	now := time.Now()
   149  	expiration := now.Add(time.Hour)
   150  
   151  	// First tick happens right away.
   152  	gotTicks = append(gotTicks, now)
   153  	for tick := 1; tick <= 19; tick++ {
   154  		gotTicks = append(gotTicks, nextTick(tick, slowestInterval, fastestInterval,
   155  			gotTicks[tick-1], expiration))
   156  	}
   157  	wantTicks := expTickSchedule(now, fastestInterval, slowestInterval)
   158  
   159  	// To check expiration on the last tick.
   160  	gotTicks = append(gotTicks, nextTick(20, slowestInterval, fastestInterval,
   161  		expiration, expiration))
   162  	wantTicks[len(wantTicks)-1] = expiration
   163  
   164  	for i, want := range wantTicks {
   165  		got := gotTicks[i]
   166  		if want != got {
   167  			t.Fatalf("expected tick %d to be: %v, got: %v", i, want, got)
   168  		}
   169  	}
   170  }
   171  
   172  // expTickSchedule returns expected tick schedule with a certain startTime.
   173  func expTickSchedule(startTime time.Time, fastestInterval, slowestInterval time.Duration) []time.Time {
   174  	expectedTicks := [21]time.Time{} // 21 element should be enough for all our needs in these tests.
   175  	expectedTicks[0] = startTime
   176  	expectedTicks[1] = expectedTicks[0].Add(fastestInterval)
   177  	expectedTicks[2] = expectedTicks[1].Add(fastestInterval)
   178  
   179  	taper := func(i int) time.Duration {
   180  		const linearCnt = fullyTapered - fullSpeedTicks
   181  		ramp := float64(slowestInterval - fastestInterval)
   182  		return time.Duration(math.Round(float64(i) / linearCnt * ramp))
   183  	}
   184  	for i := fullSpeedTicks; i < fullyTapered-1; i++ {
   185  		expectedTicks[i] = expectedTicks[i-1].Add(fastestInterval + taper(i-2))
   186  	}
   187  	for i := fullyTapered - 1; i < len(expectedTicks); i++ {
   188  		expectedTicks[i] = expectedTicks[i-1].Add(slowestInterval)
   189  	}
   190  	return expectedTicks[:]
   191  }