Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix: retry delay not persisting between calls (#3478)

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

authored by

Dois
Jovi De Croock
and committed by
GitHub
1fabe358 93d7256a

+84 -8
+7
.changeset/witty-peas-love.md
··· 1 + --- 2 + '@urql/exchange-retry': patch 3 + --- 4 + 5 + --- 6 + 7 + Fixed the delay amount not increasing as retry count increases.
+61 -3
exchanges/retry/src/retryExchange.test.ts
··· 211 211 return fromArray([ 212 212 { 213 213 operation: forwardOp, 214 - data: queryOneData, 214 + error: queryOneError, 215 215 } as any, 216 216 { 217 217 operation: forwardOp, 218 - error: queryOneError, 218 + data: queryOneData, 219 219 } as any, 220 220 ]); 221 221 } else { 222 - expect(forwardOp.context.retry).toEqual({ count: 1, delay: null }); 222 + expect(forwardOp.context.retry).toEqual({ count: 0, delay: null }); 223 223 224 224 return fromValue({ 225 225 operation: forwardOp, ··· 396 396 expect(response.mock.calls[1][0]).toHaveProperty('context.counter', 1); 397 397 expect(response.mock.calls[2][0]).toHaveProperty('context.counter', 2); 398 398 }); 399 + 400 + it('should increase retries by initialDelayMs for each subsequent failure', () => { 401 + const errorWithNetworkError = { 402 + ...queryOneError, 403 + networkError: 'scary network error', 404 + }; 405 + const response = vi.fn((forwardOp: Operation): OperationResult => { 406 + expect(forwardOp.key).toBe(op.key); 407 + return { 408 + operation: forwardOp, 409 + // @ts-ignore 410 + error: errorWithNetworkError, 411 + }; 412 + }); 413 + 414 + const result = vi.fn(); 415 + const forward: ExchangeIO = ops$ => { 416 + return pipe(ops$, map(response)); 417 + }; 418 + 419 + const retryWith = vi.fn((_error, operation) => { 420 + return makeOperation(operation.kind, operation, { 421 + ...operation.context, 422 + counter: (operation.context?.counter || 0) + 1, 423 + }); 424 + }); 425 + 426 + const fixedDelayMs = 50; 427 + 428 + const fixedDelayOptions = { 429 + ...mockOptions, 430 + randomDelay: false, 431 + initialDelayMs: fixedDelayMs, 432 + }; 433 + 434 + pipe( 435 + retryExchange({ 436 + ...fixedDelayOptions, 437 + retryIf: undefined, 438 + retryWith, 439 + })({ 440 + forward, 441 + client, 442 + dispatchDebug, 443 + })(ops$), 444 + tap(result), 445 + publish 446 + ); 447 + 448 + next(op); 449 + 450 + // delay between each call should be increased by initialDelayMs 451 + // (e.g. if initialDelayMs is 5s, first retry is waits 5 seconds, second retry waits 10 seconds) 452 + for (let i = 1; i <= fixedDelayOptions.maxNumberAttempts; i++) { 453 + expect(response).toHaveBeenCalledTimes(i); 454 + vi.advanceTimersByTime(i * fixedDelayOptions.initialDelayMs); 455 + } 456 + });
+16 -5
exchanges/retry/src/retryExchange.ts
··· 111 111 export const retryExchange = (options: RetryExchangeOptions): Exchange => { 112 112 const { retryIf, retryWith } = options; 113 113 const MIN_DELAY = options.initialDelayMs || 1000; 114 - const MAX_DELAY = options.maxDelayMs || 15000; 114 + const MAX_DELAY = options.maxDelayMs || 15_000; 115 115 const MAX_ATTEMPTS = options.maxNumberAttempts || 2; 116 116 const RANDOM_DELAY = 117 117 options.randomDelay != null ? !!options.randomDelay : true; ··· 133 133 let delayAmount = retry.delay || MIN_DELAY; 134 134 135 135 const backoffFactor = Math.random() + 1.5; 136 - // if randomDelay is enabled and it won't exceed the max delay, apply a random 137 - // amount to the delay to avoid thundering herd problem 138 - if (RANDOM_DELAY && delayAmount * backoffFactor < MAX_DELAY) { 139 - delayAmount *= backoffFactor; 136 + if (RANDOM_DELAY) { 137 + // if randomDelay is enabled and it won't exceed the max delay, apply a random 138 + // amount to the delay to avoid thundering herd problem 139 + if (delayAmount * backoffFactor < MAX_DELAY) { 140 + delayAmount *= backoffFactor; 141 + } else { 142 + delayAmount = MAX_DELAY; 143 + } 144 + } else { 145 + // otherwise, increase the delay proportionately by the initial delay 146 + delayAmount = Math.min(retryCount * MIN_DELAY, MAX_DELAY); 140 147 } 148 + 149 + // ensure the delay is carried over to the next context 150 + retry.delay = delayAmount; 141 151 142 152 // We stop the retries if a teardown event for this operation comes in 143 153 // But if this event comes through regularly we also stop the retries, since it's ··· 158 168 operation, 159 169 data: { 160 170 retryCount, 171 + delayAmount, 161 172 }, 162 173 }); 163 174