Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 103 additions & 242 deletions packages/retry-strategies/README.md

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion packages/retry-strategies/src/backoff/constant-backoff.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, suite, type TestContext, test } from "node:test";

import { ConstantBackoff } from "./constant-backoff.ts";
import { ConstantBackoff, constant } from "./constant-backoff.ts";

/* node:coverage disable */
suite("Constant backoff strategy (Unit)", () => {
Expand Down Expand Up @@ -180,4 +180,29 @@ suite("Constant backoff strategy (Unit)", () => {
);
});
});

describe("factory function", () => {
test("creates ConstantBackoff instance", (ctx: TestContext) => {
ctx.plan(3);

// Arrange & Act
const strategy = constant(1000);

// Assert
ctx.assert.ok(
strategy instanceof ConstantBackoff,
"should return ConstantBackoff instance",
);
ctx.assert.strictEqual(
strategy.nextBackoff(),
1000,
"should work correctly",
);
ctx.assert.strictEqual(
strategy.nextBackoff(),
1000,
"should maintain constant delay",
);
});
});
});
20 changes: 14 additions & 6 deletions packages/retry-strategies/src/backoff/constant-backoff.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { BackoffStrategy } from "./interface.ts";

/**
* A backoff policy that always returns the same backoff delay.
* Always returns the same delay.
*/
export class ConstantBackoff implements BackoffStrategy {
private readonly delay: number;

/**
* Creates a new ConstantBackoff instance.
*
* @param delay - The constant delay in milliseconds to return for each backoff (must be >= 0)
* @throws {RangeError} If delay is NaN or less than 0
* @param delay - Constant delay in milliseconds (>= 0)
* @throws {RangeError} If delay is invalid
*/
public constructor(delay: number) {
if (Number.isNaN(delay)) {
Expand All @@ -24,19 +24,27 @@ export class ConstantBackoff implements BackoffStrategy {

/**
* Calculate the next backoff delay.
* Always returns the configured constant delay.
*
* @returns The constant delay in milliseconds
* @returns Constant delay in milliseconds
*/
public nextBackoff(): number {
return this.delay;
}

/**
* Reset to the initial state.
* Since ConstantBackoff has no mutable state, this is a no-op.
*/
public resetBackoff(): void {
// No-op: ConstantBackoff has no mutable state to reset
}
}

/**
* Always returns the same delay.
*
* @param delay - Constant delay in milliseconds (>= 0)
* @returns ConstantBackoff instance
* @throws {RangeError} If delay is invalid
*/
export const constant = (delay: number): ConstantBackoff =>
new ConstantBackoff(delay);
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, suite, type TestContext, test } from "node:test";

import { DecorrelatedJitterBackoff } from "./decorrelated-jitter-backoff.ts";
import {
DecorrelatedJitterBackoff,
decorrelatedJitter,
} from "./decorrelated-jitter-backoff.ts";

/* node:coverage disable */
suite("Decorrelated jitter backoff strategy (Unit)", () => {
Expand Down Expand Up @@ -472,4 +475,25 @@ suite("Decorrelated jitter backoff strategy (Unit)", () => {
);
});
});

describe("factory function", () => {
test("creates DecorrelatedJitterBackoff instance", (ctx: TestContext) => {
ctx.plan(2);

// Arrange & Act
const strategy = decorrelatedJitter(100, 5000);

// Assert
ctx.assert.ok(
strategy instanceof DecorrelatedJitterBackoff,
"should return DecorrelatedJitterBackoff instance",
);
// Just verify it returns a number (jitter is random)
const delay = strategy.nextBackoff();
ctx.assert.ok(
typeof delay === "number" && delay >= 0,
"should work correctly",
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { BackoffStrategy } from "./interface.ts";

/**
* A backoff policy that uses the AWS DecorrelatedJitter algorithm.
* AWS DecorrelatedJitter algorithm - each delay based on previous delay.
* Decorrelates retry attempts to avoid synchronization between clients.
* Generally results in shorter overall wait times.
*
* The delay for attempt n is: min(cap, random(base, previous_delay * 3))
*
* This strategy decorrelates the retry attempts from each other, making the delays
* unpredictable and helping to avoid synchronization between multiple clients.
* It generally results in shorter overall wait times compared to other strategies.
* Formula: `min(cap, random(base, previousDelay * 3))`
*
* @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter}
*/
Expand All @@ -19,9 +17,9 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy {
/**
* Creates a new DecorrelatedJitterBackoff instance.
*
* @param base - The base delay in milliseconds (must be >= 0)
* @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity)
* @throws {RangeError} If base or cap is NaN or invalid
* @param base - Base delay in milliseconds (>= 0)
* @param cap - Maximum delay in milliseconds (>= base, default: Infinity)
* @throws {RangeError} If base or cap is invalid
*/
public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) {
if (Number.isNaN(base)) {
Expand All @@ -47,9 +45,8 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy {

/**
* Calculate the next backoff delay.
* Returns a random delay between base and triple the previous delay, capped at maximum.
*
* @returns The next delay in milliseconds: min(cap, random(base, previous_delay * 3))
* @returns Random delay in milliseconds: `min(cap, random(base, previousDelay * 3))`
*/
public nextBackoff(): number {
const upperBound = this.previousDelay * 3;
Expand All @@ -63,9 +60,27 @@ export class DecorrelatedJitterBackoff implements BackoffStrategy {

/**
* Reset to the initial state.
* Resets the previous delay to the base delay.
*/
public resetBackoff(): void {
this.previousDelay = this.base;
}
}

/**
* AWS DecorrelatedJitter algorithm - each delay based on previous delay.
* Formula: `min(cap, random(base, previousDelay * 3))`
*
* Decorrelates retry attempts to avoid synchronization between clients.
* Generally results in shorter overall wait times.
*
* @param base - Base delay in milliseconds (>= 0)
* @param cap - Maximum delay in milliseconds (>= base, default: Infinity)
* @returns DecorrelatedJitterBackoff instance
* @throws {RangeError} If base or cap is invalid
*
* @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter}
*/
export const decorrelatedJitter = (
base: number,
cap: number = Number.POSITIVE_INFINITY,
): DecorrelatedJitterBackoff => new DecorrelatedJitterBackoff(base, cap);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, suite, type TestContext, test } from "node:test";

import { EqualJitterBackoff } from "./equal-jitter-backoff.ts";
import { EqualJitterBackoff, equalJitter } from "./equal-jitter-backoff.ts";

/* node:coverage disable */
suite("Equal jitter backoff strategy (Unit)", () => {
Expand Down Expand Up @@ -404,4 +404,25 @@ suite("Equal jitter backoff strategy (Unit)", () => {
);
});
});

describe("factory function", () => {
test("creates EqualJitterBackoff instance", (ctx: TestContext) => {
ctx.plan(2);

// Arrange & Act
const strategy = equalJitter(100, 5000);

// Assert
ctx.assert.ok(
strategy instanceof EqualJitterBackoff,
"should return EqualJitterBackoff instance",
);
// Just verify it returns a number (jitter is random)
const delay = strategy.nextBackoff();
ctx.assert.ok(
typeof delay === "number" && delay >= 0,
"should work correctly",
);
});
});
});
36 changes: 25 additions & 11 deletions packages/retry-strategies/src/backoff/equal-jitter-backoff.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { BackoffStrategy } from "./interface.ts";

/**
* A backoff policy that uses the AWS EqualJitter algorithm.
* AWS EqualJitter algorithm - balances consistency and randomness.
* Provides more predictable timing than FullJitter while still preventing thundering herd.
*
* The delay for attempt n is: temp = min(cap, base * 2 ** n), then delay = temp / 2 + random(0, temp / 2)
*
* This strategy balances between consistent delays and randomness to prevent thundering herd problems
* while maintaining more predictable timing than FullJitter.
* Formula: `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)`
*
* @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter}
*/
Expand All @@ -18,9 +16,9 @@ export class EqualJitterBackoff implements BackoffStrategy {
/**
* Creates a new EqualJitterBackoff instance.
*
* @param base - The base delay in milliseconds (must be >= 0)
* @param cap - The maximum delay in milliseconds (must be >= base, defaults to Infinity)
* @throws {RangeError} If base or cap is NaN or invalid
* @param base - Base delay in milliseconds (>= 0)
* @param cap - Maximum delay in milliseconds (>= base, default: Infinity)
* @throws {RangeError} If base or cap is invalid
*/
public constructor(base: number, cap: number = Number.POSITIVE_INFINITY) {
if (Number.isNaN(base)) {
Expand All @@ -46,9 +44,8 @@ export class EqualJitterBackoff implements BackoffStrategy {

/**
* Calculate the next backoff delay.
* Returns a delay that is half deterministic and half random.
*
* @returns The next delay in milliseconds: temp / 2 + random(0, temp / 2), where temp = min(cap, base * 2 ** attemptCount)
* @returns Delay in milliseconds: `(temp / 2) + random(0, temp / 2)` where `temp = min(cap, base * 2^n)`
*/
public nextBackoff(): number {
const temp = Math.min(this.cap, this.base * 2 ** this.attemptCount);
Expand All @@ -60,9 +57,26 @@ export class EqualJitterBackoff implements BackoffStrategy {

/**
* Reset to the initial state.
* Resets the attempt counter to 0, so the next call to nextBackoff will use attempt 0.
*/
public resetBackoff(): void {
this.attemptCount = 0;
}
}

/**
* AWS EqualJitter algorithm - balances consistency and randomness.
* Formula: `(min(cap, base * 2^n) / 2) + random(0, min(cap, base * 2^n) / 2)`
*
* Provides more predictable timing than FullJitter while still preventing thundering herd.
*
* @param base - Base delay in milliseconds (>= 0)
* @param cap - Maximum delay in milliseconds (>= base, default: Infinity)
* @returns EqualJitterBackoff instance
* @throws {RangeError} If base or cap is invalid
*
* @see {@link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ AWS Exponential Backoff And Jitter}
*/
export const equalJitter = (
base: number,
cap: number = Number.POSITIVE_INFINITY,
): EqualJitterBackoff => new EqualJitterBackoff(base, cap);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, suite, type TestContext, test } from "node:test";

import { ExponentialBackoff } from "./exponential-backoff.ts";
import { ExponentialBackoff, exponential } from "./exponential-backoff.ts";

/* node:coverage disable */
suite("Exponential backoff strategy (Unit)", () => {
Expand Down Expand Up @@ -327,4 +327,29 @@ suite("Exponential backoff strategy (Unit)", () => {
);
});
});

describe("factory function", () => {
test("creates ExponentialBackoff instance", (ctx: TestContext) => {
ctx.plan(3);

// Arrange & Act
const strategy = exponential(100, 5000);

// Assert
ctx.assert.ok(
strategy instanceof ExponentialBackoff,
"should return ExponentialBackoff instance",
);
ctx.assert.strictEqual(
strategy.nextBackoff(),
100,
"should work correctly",
);
ctx.assert.strictEqual(
strategy.nextBackoff(),
200,
"should maintain state correctly",
);
});
});
});
Loading