Validate options on Exponential Backoff
This commit is contained in:
@@ -33,6 +33,8 @@ export class ExponentialBackoff {
|
|||||||
jitter: 0.1,
|
jitter: 0.1,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ExponentialBackoff.validateOptions(this.#options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +97,60 @@ export class ExponentialBackoff {
|
|||||||
return cappedDelay - jitter;
|
return cappedDelay - jitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the options for the exponential backoff
|
||||||
|
*
|
||||||
|
* @param options - The options to validate
|
||||||
|
*
|
||||||
|
* @throws An error if the options are invalid
|
||||||
|
*/
|
||||||
|
public static validateOptions(options: ExponentialBackoffOptions): void {
|
||||||
|
// Validate the max delay is a finite number not less than 0
|
||||||
|
if (!Number.isFinite(options.maxDelay)) {
|
||||||
|
throw new Error('maxDelay must be a finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.maxDelay < 0) {
|
||||||
|
throw new Error('maxDelay must be not less than 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the max attempts is a finite number not less than 0
|
||||||
|
if (!Number.isFinite(options.maxAttempts)) {
|
||||||
|
throw new Error('maxAttempts must be a finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.maxAttempts < 0) {
|
||||||
|
throw new Error('maxAttempts must be not less than 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the base delay is a finite number not less than 0
|
||||||
|
if (!Number.isFinite(options.baseDelay)) {
|
||||||
|
throw new Error('baseDelay must be a finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.baseDelay < 0) {
|
||||||
|
throw new Error('baseDelay must be not less than 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the growth rate is a finite number not less than 0
|
||||||
|
if (!Number.isFinite(options.growthRate)) {
|
||||||
|
throw new Error('growthRate must be a finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.growthRate < 0) {
|
||||||
|
throw new Error('growthRate must be not less than 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the jitter is a finite number not less than 0 or greater than 1
|
||||||
|
if (!Number.isFinite(options.jitter)) {
|
||||||
|
throw new Error('jitter must be a finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.jitter < 0 || options.jitter > 1) {
|
||||||
|
throw new Error('jitter must be not less than 0 or greater than 1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the function with exponential backoff
|
* Run the function with exponential backoff
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ import { expect, test, vi } from 'vitest';
|
|||||||
import { ExponentialBackoff } from '../source/exponential-backoff.ts';
|
import { ExponentialBackoff } from '../source/exponential-backoff.ts';
|
||||||
import { ExponentialBackoffMaxRetriesHitError } from '../source/errors.ts';
|
import { ExponentialBackoffMaxRetriesHitError } from '../source/errors.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A valid options object that satisfies {@link ExponentialBackoff.validateOptions}.
|
||||||
|
*/
|
||||||
|
const validExponentialBackoffOptions = {
|
||||||
|
maxDelay: 10_000,
|
||||||
|
maxAttempts: 10,
|
||||||
|
baseDelay: 1_000,
|
||||||
|
growthRate: 2,
|
||||||
|
jitter: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that the static {@link ExponentialBackoff.run} helper creates a throwaway instance
|
* Tests that the static {@link ExponentialBackoff.run} helper creates a throwaway instance
|
||||||
* with library defaults (including the default 1000ms base delay) when no options are passed.
|
* with library defaults (including the default 1000ms base delay) when no options are passed.
|
||||||
@@ -98,9 +109,11 @@ const testExponentialBackoffSucceedsOnFirstAttempt = async (): Promise<void> =>
|
|||||||
*/
|
*/
|
||||||
const testExponentialBackoffRetriesUntilSuccess = async (): Promise<void> => {
|
const testExponentialBackoffRetriesUntilSuccess = async (): Promise<void> => {
|
||||||
// Three invocations: two rejections then a success on the third call.
|
// Three invocations: two rejections then a success on the third call.
|
||||||
const tripleRejectFn = vi.fn().mockRejectedValueOnce(new Error('attempt 1'))
|
const tripleRejectFn = vi
|
||||||
.mockRejectedValueOnce(new Error('attempt 2'))
|
.fn()
|
||||||
.mockResolvedValueOnce('success');
|
.mockRejectedValueOnce(new Error('attempt 1'))
|
||||||
|
.mockRejectedValueOnce(new Error('attempt 2'))
|
||||||
|
.mockResolvedValueOnce('success');
|
||||||
|
|
||||||
// maxAttempts: 5 is high enough that we stop because fn succeeded, not because we hit the cap.
|
// maxAttempts: 5 is high enough that we stop because fn succeeded, not because we hit the cap.
|
||||||
const result = await ExponentialBackoff.run(tripleRejectFn, () => {}, {
|
const result = await ExponentialBackoff.run(tripleRejectFn, () => {}, {
|
||||||
@@ -351,6 +364,116 @@ const testExponentialBackoffAppliesJitter = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that {@link ExponentialBackoff.validateOptions} accepts valid options, including boundary values of 0 and 1.
|
||||||
|
*/
|
||||||
|
const testExponentialBackoffValidateOptionsAcceptsValidOptions = (): void => {
|
||||||
|
const validCases = [
|
||||||
|
validExponentialBackoffOptions,
|
||||||
|
{
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
maxDelay: 0,
|
||||||
|
maxAttempts: 0,
|
||||||
|
baseDelay: 0,
|
||||||
|
growthRate: 0,
|
||||||
|
jitter: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
jitter: 1,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const options of validCases) {
|
||||||
|
expect(() => ExponentialBackoff.validateOptions(options)).not.toThrow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that {@link ExponentialBackoff.validateOptions} rejects negative numeric options.
|
||||||
|
*/
|
||||||
|
const testExponentialBackoffValidateOptionsRejectsNegativeValues = (): void => {
|
||||||
|
// Define our test cases with each value being less than 0
|
||||||
|
const negativeCases = [
|
||||||
|
{ field: 'maxDelay', value: -1 },
|
||||||
|
{ field: 'maxAttempts', value: -1 },
|
||||||
|
{ field: 'baseDelay', value: -1 },
|
||||||
|
{ field: 'growthRate', value: -1 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Iterate through the test cases and expect an error to be thrown
|
||||||
|
for (const { field, value } of negativeCases) {
|
||||||
|
expect(() =>
|
||||||
|
ExponentialBackoff.validateOptions({
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
[field]: value,
|
||||||
|
})).toThrow(`${field} must be not less than 0`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that {@link ExponentialBackoff.validateOptions} rejects jitter below 0 or above 1.
|
||||||
|
*/
|
||||||
|
const testExponentialBackoffValidateOptionsRejectsInvalidJitter = (): void => {
|
||||||
|
// Define our test cases with each value being less than 0 or greater than 1
|
||||||
|
const invalidJitterCases: Array<{ value: number }> = [{ value: -0.1 }, { value: 1.1 }];
|
||||||
|
|
||||||
|
// Iterate through the test cases and expect an error to be thrown
|
||||||
|
for (const { value } of invalidJitterCases) {
|
||||||
|
expect(() =>
|
||||||
|
ExponentialBackoff.validateOptions({
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
jitter: value,
|
||||||
|
})).toThrow('jitter must be not less than 0 or greater than 1');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that {@link ExponentialBackoff.validateOptions} rejects non-finite values such as Infinity.
|
||||||
|
*/
|
||||||
|
const testExponentialBackoffValidateOptionsRejectsNonFiniteValues = (): void => {
|
||||||
|
// Define our test cases with each value being Infinity
|
||||||
|
const nonFiniteCases = [
|
||||||
|
{ field: 'maxDelay', value: Infinity },
|
||||||
|
{ field: 'maxAttempts', value: Infinity },
|
||||||
|
{ field: 'baseDelay', value: Infinity },
|
||||||
|
{ field: 'growthRate', value: Infinity },
|
||||||
|
{ field: 'jitter', value: Infinity },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Iterate through the test cases and expect an error to be thrown
|
||||||
|
for (const { field, value } of nonFiniteCases) {
|
||||||
|
expect(() =>
|
||||||
|
ExponentialBackoff.validateOptions({
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
[field]: value,
|
||||||
|
})).toThrow(`${field} must be a finite number`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that {@link ExponentialBackoff.validateOptions} rejects NaN, which is also non-finite.
|
||||||
|
*/
|
||||||
|
const testExponentialBackoffValidateOptionsRejectsNaN = (): void => {
|
||||||
|
// Define our test cases with each value being NaN
|
||||||
|
const nanCases = [
|
||||||
|
{ field: 'maxDelay', value: Number.NaN },
|
||||||
|
{ field: 'maxAttempts', value: Number.NaN },
|
||||||
|
{ field: 'baseDelay', value: Number.NaN },
|
||||||
|
{ field: 'growthRate', value: Number.NaN },
|
||||||
|
{ field: 'jitter', value: Number.NaN },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Iterate through the test cases and expect an error to be thrown
|
||||||
|
for (const { field, value } of nanCases) {
|
||||||
|
expect(() =>
|
||||||
|
ExponentialBackoff.validateOptions({
|
||||||
|
...validExponentialBackoffOptions,
|
||||||
|
[field]: value,
|
||||||
|
})).toThrow(`${field} must be a finite number`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runTests = async (): Promise<void> => {
|
const runTests = async (): Promise<void> => {
|
||||||
test('ExponentialBackoff.run: delegates to a new instance using default options', testExponentialBackoffRunUsesDefaultOptions);
|
test('ExponentialBackoff.run: delegates to a new instance using default options', testExponentialBackoffRunUsesDefaultOptions);
|
||||||
test('ExponentialBackoff.run: retries and succeeds with partial options', testExponentialBackoffRunWithPartialOptions);
|
test('ExponentialBackoff.run: retries and succeeds with partial options', testExponentialBackoffRunWithPartialOptions);
|
||||||
@@ -368,6 +491,11 @@ const runTests = async (): Promise<void> => {
|
|||||||
test('ExponentialBackoff: increases delay exponentially between attempts', testExponentialBackoffIncreasesDelayExponentially);
|
test('ExponentialBackoff: increases delay exponentially between attempts', testExponentialBackoffIncreasesDelayExponentially);
|
||||||
test('ExponentialBackoff: caps delay at maxDelay', testExponentialBackoffCapsDelayAtMaxDelay);
|
test('ExponentialBackoff: caps delay at maxDelay', testExponentialBackoffCapsDelayAtMaxDelay);
|
||||||
test('ExponentialBackoff: subtracts jitter from the capped delay', testExponentialBackoffAppliesJitter);
|
test('ExponentialBackoff: subtracts jitter from the capped delay', testExponentialBackoffAppliesJitter);
|
||||||
|
test('ExponentialBackoff.validateOptions: accepts valid options', testExponentialBackoffValidateOptionsAcceptsValidOptions);
|
||||||
|
test('ExponentialBackoff.validateOptions: rejects negative values', testExponentialBackoffValidateOptionsRejectsNegativeValues);
|
||||||
|
test('ExponentialBackoff.validateOptions: rejects invalid jitter', testExponentialBackoffValidateOptionsRejectsInvalidJitter);
|
||||||
|
test('ExponentialBackoff.validateOptions: rejects Infinity', testExponentialBackoffValidateOptionsRejectsNonFiniteValues);
|
||||||
|
test('ExponentialBackoff.validateOptions: rejects NaN', testExponentialBackoffValidateOptionsRejectsNaN);
|
||||||
};
|
};
|
||||||
|
|
||||||
await runTests();
|
await runTests();
|
||||||
|
|||||||
Reference in New Issue
Block a user