Skip to content

Commit aa3ca1d

Browse files
Add generateJitter utility function and update QBORepository to use it
- Introduced `generateJitter` function in `utils.ts` to create cryptographically secure random jitter for retry delays, enhancing the reliability of retry logic. - Updated `QBORepository` to utilize the new `generateJitter` function instead of a manual jitter calculation, improving code clarity and maintainability. - Added unit tests for `generateJitter` to ensure it generates values within the expected range and behaves consistently across multiple calls. These changes enhance the utility functions available for managing delays in API calls, contributing to a more robust integration with QuickBooks Online.
1 parent 7387bf5 commit aa3ca1d

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

workers/main/src/common/utils.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ vi.mock('../configs', () => ({
55
}));
66

77
import * as configs from '../configs';
8-
import { formatDateToISOString, validateEnv } from './utils';
8+
import { formatDateToISOString, generateJitter, validateEnv } from './utils';
99

1010
type ValidationResult = {
1111
success: boolean;
@@ -20,7 +20,7 @@ describe('validateEnv', () => {
2020
let exitSpy: ReturnType<typeof vi.spyOn>;
2121

2222
beforeEach(() => {
23-
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
23+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
2424
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
2525
throw new Error('exit');
2626
}) as unknown as ReturnType<typeof vi.spyOn>;
@@ -51,7 +51,7 @@ describe('validateEnv', () => {
5151
expect(() => validateEnv()).toThrow('exit');
5252
expect(errorSpy).toHaveBeenCalledWith(
5353
'Missing or invalid environment variable: FOO (is required)\n' +
54-
'Missing or invalid environment variable: (unknown variable) (unknown)',
54+
'Missing or invalid environment variable: (unknown variable) (unknown)',
5555
);
5656
expect(exitSpy).toHaveBeenCalledWith(1);
5757
});
@@ -79,3 +79,39 @@ describe('formatDateToISOString', () => {
7979
expect(result).toBe('2024-12-31');
8080
});
8181
});
82+
83+
describe('generateJitter', () => {
84+
it('generates jitter within expected range', () => {
85+
const baseDelay = 1000;
86+
const jitter = generateJitter(baseDelay);
87+
88+
// Jitter should be between 0 and 10% of baseDelay
89+
expect(jitter).toBeGreaterThanOrEqual(0);
90+
expect(jitter).toBeLessThanOrEqual(0.1 * baseDelay);
91+
expect(jitter).toBeLessThan(100); // 10% of 1000ms
92+
});
93+
94+
it('generates different jitter values on multiple calls', () => {
95+
const baseDelay = 2000;
96+
const jitter1 = generateJitter(baseDelay);
97+
const jitter2 = generateJitter(baseDelay);
98+
99+
// Values should be different (cryptographically random)
100+
expect(jitter1).not.toBe(jitter2);
101+
});
102+
103+
it('scales jitter proportionally with base delay', () => {
104+
const smallDelay = 500;
105+
const largeDelay = 2000;
106+
107+
const smallJitter = generateJitter(smallDelay);
108+
const largeJitter = generateJitter(largeDelay);
109+
110+
// Large delay should produce larger jitter
111+
expect(largeJitter).toBeGreaterThan(smallJitter);
112+
113+
// Both should be within 10% of their respective base delays
114+
expect(smallJitter).toBeLessThanOrEqual(0.1 * smallDelay);
115+
expect(largeJitter).toBeLessThanOrEqual(0.1 * largeDelay);
116+
});
117+
});

workers/main/src/common/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import crypto from 'crypto';
2+
13
import { validationResult } from '../configs';
24

35
export function validateEnv() {
@@ -21,3 +23,15 @@ export function formatDateToISOString(date: Date): string {
2123

2224
return `${year}-${month}-${day}`;
2325
}
26+
27+
/**
28+
* Generates cryptographically secure random jitter for retry delays
29+
* @param baseDelay - The base delay in milliseconds
30+
* @returns A random jitter value between 0 and 10% of the base delay
31+
*/
32+
export function generateJitter(baseDelay: number): number {
33+
const randomBytes = crypto.randomBytes(4);
34+
const randomValue = randomBytes.readUInt32BE(0) / 0xffffffff; // Convert to 0-1 range
35+
36+
return randomValue * 0.1 * baseDelay;
37+
}

workers/main/src/services/QBO/QBORepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import axios from 'axios';
22
import axiosRetry from 'axios-retry';
33

44
import { QuickBooksRepositoryError } from '../../common/errors';
5-
import { formatDateToISOString } from '../../common/utils';
5+
import { formatDateToISOString, generateJitter } from '../../common/utils';
66
import { axiosConfig } from '../../configs/axios';
77
import { qboConfig } from '../../configs/qbo';
88
import { OAuth2Manager } from '../OAuth2';
@@ -72,7 +72,7 @@ export class QBORepository implements IQBORepository {
7272
if (retryAfter) return parseInt(retryAfter) * 1000;
7373
}
7474
const baseDelay = Math.pow(2, attempt) * 1000;
75-
const jitter = Math.random() * 0.1 * baseDelay;
75+
const jitter = generateJitter(baseDelay);
7676
const maxDelay = error.response?.status === 502 ? 60000 : 30000;
7777

7878
return Math.min(baseDelay + jitter, maxDelay);

0 commit comments

Comments
 (0)