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>

Dois 1fabe358 93d7256a

Changed files
+84 -8
.changeset
exchanges
+7
.changeset/witty-peas-love.md
···
+
---
+
'@urql/exchange-retry': patch
+
---
+
+
---
+
+
Fixed the delay amount not increasing as retry count increases.
+61 -3
exchanges/retry/src/retryExchange.test.ts
···
return fromArray([
{
operation: forwardOp,
-
data: queryOneData,
+
error: queryOneError,
} as any,
{
operation: forwardOp,
-
error: queryOneError,
+
data: queryOneData,
} as any,
]);
} else {
-
expect(forwardOp.context.retry).toEqual({ count: 1, delay: null });
+
expect(forwardOp.context.retry).toEqual({ count: 0, delay: null });
return fromValue({
operation: forwardOp,
···
expect(response.mock.calls[1][0]).toHaveProperty('context.counter', 1);
expect(response.mock.calls[2][0]).toHaveProperty('context.counter', 2);
});
+
+
it('should increase retries by initialDelayMs for each subsequent failure', () => {
+
const errorWithNetworkError = {
+
...queryOneError,
+
networkError: 'scary network error',
+
};
+
const response = vi.fn((forwardOp: Operation): OperationResult => {
+
expect(forwardOp.key).toBe(op.key);
+
return {
+
operation: forwardOp,
+
// @ts-ignore
+
error: errorWithNetworkError,
+
};
+
});
+
+
const result = vi.fn();
+
const forward: ExchangeIO = ops$ => {
+
return pipe(ops$, map(response));
+
};
+
+
const retryWith = vi.fn((_error, operation) => {
+
return makeOperation(operation.kind, operation, {
+
...operation.context,
+
counter: (operation.context?.counter || 0) + 1,
+
});
+
});
+
+
const fixedDelayMs = 50;
+
+
const fixedDelayOptions = {
+
...mockOptions,
+
randomDelay: false,
+
initialDelayMs: fixedDelayMs,
+
};
+
+
pipe(
+
retryExchange({
+
...fixedDelayOptions,
+
retryIf: undefined,
+
retryWith,
+
})({
+
forward,
+
client,
+
dispatchDebug,
+
})(ops$),
+
tap(result),
+
publish
+
);
+
+
next(op);
+
+
// delay between each call should be increased by initialDelayMs
+
// (e.g. if initialDelayMs is 5s, first retry is waits 5 seconds, second retry waits 10 seconds)
+
for (let i = 1; i <= fixedDelayOptions.maxNumberAttempts; i++) {
+
expect(response).toHaveBeenCalledTimes(i);
+
vi.advanceTimersByTime(i * fixedDelayOptions.initialDelayMs);
+
}
+
});
+16 -5
exchanges/retry/src/retryExchange.ts
···
export const retryExchange = (options: RetryExchangeOptions): Exchange => {
const { retryIf, retryWith } = options;
const MIN_DELAY = options.initialDelayMs || 1000;
-
const MAX_DELAY = options.maxDelayMs || 15000;
+
const MAX_DELAY = options.maxDelayMs || 15_000;
const MAX_ATTEMPTS = options.maxNumberAttempts || 2;
const RANDOM_DELAY =
options.randomDelay != null ? !!options.randomDelay : true;
···
let delayAmount = retry.delay || MIN_DELAY;
const backoffFactor = Math.random() + 1.5;
-
// if randomDelay is enabled and it won't exceed the max delay, apply a random
-
// amount to the delay to avoid thundering herd problem
-
if (RANDOM_DELAY && delayAmount * backoffFactor < MAX_DELAY) {
-
delayAmount *= backoffFactor;
+
if (RANDOM_DELAY) {
+
// if randomDelay is enabled and it won't exceed the max delay, apply a random
+
// amount to the delay to avoid thundering herd problem
+
if (delayAmount * backoffFactor < MAX_DELAY) {
+
delayAmount *= backoffFactor;
+
} else {
+
delayAmount = MAX_DELAY;
+
}
+
} else {
+
// otherwise, increase the delay proportionately by the initial delay
+
delayAmount = Math.min(retryCount * MIN_DELAY, MAX_DELAY);
}
+
+
// ensure the delay is carried over to the next context
+
retry.delay = delayAmount;
// We stop the retries if a teardown event for this operation comes in
// But if this event comes through regularly we also stop the retries, since it's
···
operation,
data: {
retryCount,
+
delayAmount,
},
});