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 }