Skip to content

Commit

Permalink
fix(util-retry): use token instances with shared token availability
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed May 24, 2023
1 parent 4154bf9 commit 2269a41
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 30 deletions.
8 changes: 6 additions & 2 deletions packages/util-retry/src/StandardRetryStrategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ describe(StandardRetryStrategy.name, () => {
describe("retryToken init", () => {
it("sets retryToken", () => {
const retryStrategy = new StandardRetryStrategy(() => Promise.resolve(maxAttempts));
expect(retryStrategy["retryToken"]).toBe(getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE));
expect(retryStrategy["retryToken"]).toBe(
getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE)
);
});
});

describe("acquireInitialRetryToken", () => {
it("returns default retryToken", async () => {
const retryStrategy = new StandardRetryStrategy(() => Promise.resolve(maxAttempts));
const retryToken = await retryStrategy.acquireInitialRetryToken(retryTokenScope);
expect(retryToken).toEqual(getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE));
expect(retryToken).toEqual(
getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE)
);
});
});

Expand Down
5 changes: 3 additions & 2 deletions packages/util-retry/src/StandardRetryStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import { getDefaultRetryToken } from "./defaultRetryToken";
export class StandardRetryStrategy implements RetryStrategyV2 {
public readonly mode: string = RETRY_MODES.STANDARD;
private retryToken: StandardRetryToken;
private availableCapacityRef = { availableCapacity: INITIAL_RETRY_TOKENS };
private readonly maxAttemptsProvider: Provider<number>;

constructor(maxAttempts: number);
constructor(maxAttemptsProvider: Provider<number>);
constructor(private readonly maxAttempts: number | Provider<number>) {
this.retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
this.retryToken = getDefaultRetryToken(this.availableCapacityRef, DEFAULT_RETRY_DELAY_BASE);
this.maxAttemptsProvider = typeof maxAttempts === "function" ? maxAttempts : async () => maxAttempts;
}

public async acquireInitialRetryToken(retryTokenScope: string): Promise<StandardRetryToken> {
return this.retryToken;
return (this.retryToken = getDefaultRetryToken(this.availableCapacityRef, DEFAULT_RETRY_DELAY_BASE));
}

public async refreshRetryTokenForRetry(
Expand Down
45 changes: 26 additions & 19 deletions packages/util-retry/src/defaultRetryToken.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("defaultRetryToken", () => {
error: RetryErrorInfo,
initialRetryTokens: number = INITIAL_RETRY_TOKENS
) => {
const retryToken = getDefaultRetryToken(initialRetryTokens, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: initialRetryTokens }, DEFAULT_RETRY_DELAY_BASE);
let availableCapacity = initialRetryTokens;
while (availableCapacity >= targetCapacity) {
retryToken.getRetryTokenCount(error);
Expand All @@ -49,13 +49,13 @@ describe("defaultRetryToken", () => {
describe("custom initial retry tokens", () => {
it("hasRetryTokens returns false if capacity is not available", () => {
const customRetryTokens = 5;
const retryToken = getDefaultRetryToken(customRetryTokens, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: customRetryTokens }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.hasRetryTokens(transientErrorType)).toBe(false);
});

it("retrieveRetryToken throws error if retry tokens not available", () => {
const customRetryTokens = 5;
const retryToken = getDefaultRetryToken(customRetryTokens, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: customRetryTokens }, DEFAULT_RETRY_DELAY_BASE);
expect(() => {
retryToken.getRetryTokenCount({ errorType: transientErrorType });
}).toThrowError(new Error("No retry token available"));
Expand All @@ -65,12 +65,12 @@ describe("defaultRetryToken", () => {
describe("hasRetryTokens", () => {
describe("returns true if capacity is available", () => {
it("when it's transient error", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.hasRetryTokens(transientErrorType)).toBe(true);
});

it("when it's not transient error", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.hasRetryTokens(nonTransientErrorType)).toBe(true);
});
});
Expand All @@ -91,13 +91,13 @@ describe("defaultRetryToken", () => {
describe("retrieveRetryToken", () => {
describe("returns retry tokens amount if available", () => {
it("when it's transient error", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.getRetryTokenCount({ errorType: transientErrorType })).toBe(TIMEOUT_RETRY_COST);
expect(retryToken.getLastRetryCost()).toBe(TIMEOUT_RETRY_COST);
});

it("when it's not transient error", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.getRetryTokenCount({ errorType: nonTransientErrorType })).toBe(RETRY_COST);
expect(retryToken.getLastRetryCost()).toBe(RETRY_COST);
});
Expand All @@ -122,12 +122,12 @@ describe("defaultRetryToken", () => {

describe("getLastRetryCost", () => {
it("is undefined before an error is encountered", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.getLastRetryCost()).toBeUndefined();
});

it("is updated with successive errors", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
retryToken.getRetryTokenCount({ errorType: transientErrorType });
expect(retryToken.getLastRetryCost()).toBe(TIMEOUT_RETRY_COST);
retryToken.getRetryTokenCount({ errorType: nonTransientErrorType });
Expand All @@ -137,18 +137,22 @@ describe("defaultRetryToken", () => {

describe("getRetryCount", () => {
it("returns 0 when count is not set", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.getRetryCount()).toBe(0);
});

it("returns amount set when token is created", () => {
const retryCount = 3;
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE, retryCount);
const retryToken = getDefaultRetryToken(
{ availableCapacity: INITIAL_RETRY_TOKENS },
DEFAULT_RETRY_DELAY_BASE,
retryCount
);
expect(retryToken.getRetryCount()).toBe(retryCount);
});

it("increments when retries occur", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE, 1);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE, 1);
expect(retryToken.getRetryCount()).toBe(1);
retryToken.getRetryTokenCount({ errorType: transientErrorType });
expect(retryToken.getRetryCount()).toBe(2);
Expand All @@ -159,7 +163,7 @@ describe("defaultRetryToken", () => {

describe("getRetryDelay", () => {
it("returns initial delay", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
expect(retryToken.getRetryDelay()).toBe(DEFAULT_RETRY_DELAY_BASE);
});

Expand All @@ -175,7 +179,7 @@ describe("defaultRetryToken", () => {
setDelayBase,
};
(getDefaultRetryBackoffStrategy as jest.Mock).mockReturnValue(mockRetryBackoffStrategy);
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
[0, 1, 2, 3].forEach((attempts) => {
const mockDelayBase = 100;
const expectedDelay = Math.floor(2 ** attempts * mockDelayBase);
Expand All @@ -199,7 +203,7 @@ describe("defaultRetryToken", () => {
setDelayBase,
};
(getDefaultRetryBackoffStrategy as jest.Mock).mockReturnValue(mockRetryBackoffStrategy);
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
[0, 1, 2, 3].forEach((attempts) => {
const mockDelayBase = 500;
const expectedDelay = Math.floor(2 ** attempts * mockDelayBase);
Expand All @@ -213,7 +217,10 @@ describe("defaultRetryToken", () => {

describe(`caps retry delay at ${MAXIMUM_RETRY_DELAY / 1000} seconds`, () => {
it("when value exceeded because of high delayBase", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE * 1000);
const retryToken = getDefaultRetryToken(
{ availableCapacity: INITIAL_RETRY_TOKENS },
DEFAULT_RETRY_DELAY_BASE * 1000
);
expect(retryToken.getRetryDelay()).toBe(MAXIMUM_RETRY_DELAY);
});

Expand All @@ -226,7 +233,7 @@ describe("defaultRetryToken", () => {
(getDefaultRetryBackoffStrategy as jest.Mock).mockReturnValue(mockRetryBackoffStrategy);
const largeAttemptsNumber = Math.ceil(Math.log2(MAXIMUM_RETRY_DELAY));
const retryToken = getDefaultRetryToken(
INITIAL_RETRY_TOKENS * largeAttemptsNumber,
{ availableCapacity: INITIAL_RETRY_TOKENS * largeAttemptsNumber },
DEFAULT_RETRY_DELAY_BASE,
largeAttemptsNumber
);
Expand All @@ -236,7 +243,7 @@ describe("defaultRetryToken", () => {
});

it("uses retry-after hint", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
// 5 minutes, greater than maximum allowed for normal retry.
const expectedDelay = 5 * 60 * 1000;
const retryAfterHint = new Date(Date.now() + expectedDelay);
Expand Down Expand Up @@ -283,7 +290,7 @@ describe("defaultRetryToken", () => {
});

it("ensures availableCapacity is maxed at INITIAL_RETRY_TOKENS", () => {
const retryToken = getDefaultRetryToken(INITIAL_RETRY_TOKENS, DEFAULT_RETRY_DELAY_BASE);
const retryToken = getDefaultRetryToken({ availableCapacity: INITIAL_RETRY_TOKENS }, DEFAULT_RETRY_DELAY_BASE);
const { errorType } = { errorType: nonTransientErrorType };

// release 100 tokens.
Expand Down
16 changes: 9 additions & 7 deletions packages/util-retry/src/defaultRetryToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ export interface DefaultRetryTokenOptions {
* @internal
*/
export const getDefaultRetryToken = (
initialRetryTokens: number,
availableCapacityRef: {
availableCapacity: number;
},
initialRetryDelay: number,
initialRetryCount?: number,
options?: DefaultRetryTokenOptions
): StandardRetryToken => {
const MAX_CAPACITY = initialRetryTokens;
const MAX_CAPACITY = availableCapacityRef.availableCapacity;
const retryCost = options?.retryCost ?? RETRY_COST;
const timeoutRetryCost = options?.timeoutRetryCost ?? TIMEOUT_RETRY_COST;
const retryBackoffStrategy = options?.retryBackoffStrategy ?? getDefaultRetryBackoffStrategy();

let availableCapacity = initialRetryTokens;
let retryDelay = Math.min(MAXIMUM_RETRY_DELAY, initialRetryDelay);
let lastRetryCost: number | undefined = undefined;
let retryCount = initialRetryCount ?? 0;
Expand All @@ -58,7 +59,8 @@ export const getDefaultRetryToken = (

const getLastRetryCost = (): number | undefined => lastRetryCost;

const hasRetryTokens = (errorType: RetryErrorType): boolean => getCapacityAmount(errorType) <= availableCapacity;
const hasRetryTokens = (errorType: RetryErrorType): boolean =>
getCapacityAmount(errorType) <= availableCapacityRef.availableCapacity;

const getRetryTokenCount = (errorInfo: RetryErrorInfo) => {
const errorType = errorInfo.errorType;
Expand All @@ -77,13 +79,13 @@ export const getDefaultRetryToken = (
}
retryCount++;
lastRetryCost = capacityAmount;
availableCapacity -= capacityAmount;
availableCapacityRef.availableCapacity -= capacityAmount;
return capacityAmount;
};

const releaseRetryTokens = (releaseAmount?: number) => {
availableCapacity += releaseAmount ?? NO_RETRY_INCREMENT;
availableCapacity = Math.min(availableCapacity, MAX_CAPACITY);
availableCapacityRef.availableCapacity += releaseAmount ?? NO_RETRY_INCREMENT;
availableCapacityRef.availableCapacity = Math.min(availableCapacityRef.availableCapacity, MAX_CAPACITY);
};

return {
Expand Down

0 comments on commit 2269a41

Please sign in to comment.