Skip to content

Commit 218bd03

Browse files
authored
Merge pull request #593 from toniasteve15-ui/feat/585-retry-delay-helper
feat: add computeRetryDelay helper for backoff with jitter (#585)
2 parents 289259e + ebff6aa commit 218bd03

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { computeRetryDelay } from '../retry-delay.utils';
2+
3+
describe('computeRetryDelay', () => {
4+
let mathRandomSpy: jest.SpyInstance;
5+
6+
beforeEach(() => {
7+
mathRandomSpy = jest.spyOn(Math, 'random');
8+
});
9+
10+
afterEach(() => {
11+
mathRandomSpy.mockRestore();
12+
});
13+
14+
it('returns approximately baseMs on attempt 0', () => {
15+
// Set random to 0 to test lower bound
16+
mathRandomSpy.mockReturnValue(0);
17+
expect(computeRetryDelay(0, 1000, 10000)).toBe(1000);
18+
19+
// Set random to 0.99 to test upper bound of jitter
20+
mathRandomSpy.mockReturnValue(0.9999);
21+
// Computed delay is 1000. Jitter is 0.9999 * (0.2 * 1000) = ~200
22+
// Expect result to be around 1199
23+
expect(computeRetryDelay(0, 1000, 10000)).toBe(1199);
24+
});
25+
26+
it('doubles the base delay on attempt 1 with jitter applied', () => {
27+
mathRandomSpy.mockReturnValue(0.5); // 10% jitter
28+
29+
// base = 1000. attempt = 1 -> exponentialDelay = 2000
30+
// jitter = 0.5 * (0.2 * 2000) = 0.5 * 400 = 200
31+
// final delay = 2000 + 200 = 2200
32+
expect(computeRetryDelay(1, 1000, 10000)).toBe(2200);
33+
});
34+
35+
it('doubles the delay on attempt 2', () => {
36+
mathRandomSpy.mockReturnValue(0);
37+
38+
// base = 1000. attempt = 2 -> exponentialDelay = 4000
39+
// jitter = 0
40+
expect(computeRetryDelay(2, 1000, 10000)).toBe(4000);
41+
});
42+
43+
it('caps the delay at maxMs even without jitter', () => {
44+
mathRandomSpy.mockReturnValue(0);
45+
46+
// attempt 4 -> 1000 * 2^4 = 16000. Should cap at 10000
47+
expect(computeRetryDelay(4, 1000, 10000)).toBe(10000);
48+
});
49+
50+
it('never exceeds maxMs even when jitter is applied at the max boundary', () => {
51+
mathRandomSpy.mockReturnValue(0.9999);
52+
53+
// attempt 4 -> 16000. Caps at 10000.
54+
// jitter is applied to 10000 -> up to 2000.
55+
// Result would be 12000, but should be clamped to maxMs (10000).
56+
expect(computeRetryDelay(4, 1000, 10000)).toBe(10000);
57+
58+
// Let's test a case right below maxMs where jitter would push it over
59+
// base = 1000, attempt = 3 -> 8000.
60+
// Max is 9000.
61+
// jitter on 8000 is up to 1600.
62+
// 8000 + 1600 = 9600 -> should cap at 9000.
63+
expect(computeRetryDelay(3, 1000, 9000)).toBe(9000);
64+
});
65+
});

src/utils/retry-delay.utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Computes the next retry delay using exponential backoff with jitter.
3+
*
4+
* Formula: delay = min(baseMs * 2^attempt, maxMs) + random(0, 0.2 * computed_delay)
5+
* The final delay is capped at maxMs.
6+
*
7+
* @param attempt - The current retry attempt (0-indexed).
8+
* @param baseMs - The base delay in milliseconds.
9+
* @param maxMs - The maximum allowed delay.
10+
* @returns The calculated delay in milliseconds.
11+
*/
12+
export function computeRetryDelay(attempt: number, baseMs: number, maxMs: number): number {
13+
const exponentialDelay = baseMs * Math.pow(2, attempt);
14+
const computedDelay = Math.min(exponentialDelay, maxMs);
15+
16+
// Add up to 20% jitter to the computed delay
17+
const jitter = Math.random() * (0.2 * computedDelay);
18+
19+
// Ensure the final delay with jitter never exceeds maxMs
20+
return Math.floor(Math.min(computedDelay + jitter, maxMs));
21+
}

0 commit comments

Comments
 (0)