Skip to content

Commit

Permalink
feat: Implement FallbackPolicy
Browse files Browse the repository at this point in the history
Closes #185.
  • Loading branch information
luczsoma committed Mar 16, 2020
1 parent 7e0881d commit 11410d3
Show file tree
Hide file tree
Showing 11 changed files with 403 additions and 57 deletions.
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { TimeoutException } from './policies/proactive/timeoutPolicy/timeoutException';
export { TimeoutPolicy } from './policies/proactive/timeoutPolicy/timeoutPolicy';
export { FallbackChainExhaustedException } from './policies/reactive/fallbackPolicy/fallbackChainExhaustedException';
export { FallbackPolicy } from './policies/reactive/fallbackPolicy/fallbackPolicy';
export { BackoffStrategyFactory } from './policies/reactive/retryPolicy/backoffStrategyFactory';
export { RetryPolicy } from './policies/reactive/retryPolicy/retryPolicy';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class FallbackChainExhaustedException extends Error {}
1 change: 1 addition & 0 deletions src/policies/reactive/fallbackPolicy/fallbackChainLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type FallbackChainLink<ResultType> = () => ResultType | Promise<ResultType>;
112 changes: 112 additions & 0 deletions src/policies/reactive/fallbackPolicy/fallbackPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { OnFinallyFn } from '../../../types/onFinallyFn';
import { ReactivePolicy } from '../reactivePolicy';
import { FallbackChainExhaustedException } from './fallbackChainExhaustedException';
import { FallbackChainLink } from './fallbackChainLink';
import { OnFallbackFn } from './onFallbackFn';

export class FallbackPolicy<ResultType> extends ReactivePolicy<ResultType> {
private readonly fallbackChain: Array<FallbackChainLink<ResultType>> = [];
private readonly onFallbackFns: Array<OnFallbackFn<ResultType>> = [];
private readonly onFinallyFns: OnFinallyFn[] = [];
private executing = 0;

public fallback(fallbackChainLink: FallbackChainLink<ResultType>): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}

this.fallbackChain.push(fallbackChainLink);
}

public onFallback(onFallbackFn: OnFallbackFn<ResultType>): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}

this.onFallbackFns.push(onFallbackFn);
}

public onFinally(fn: OnFinallyFn): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}

this.onFinallyFns.push(fn);
}

public async execute(fn: () => ResultType | Promise<ResultType>): Promise<ResultType> {
try {
this.executing++;

const remainingFallbackChain = [...this.fallbackChain];
let executor = fn;

// eslint-disable-next-line no-constant-condition
while (true) {
try {
const result = await executor();

const shouldFallbackOnResult = await this.isResultHandled(result);
if (!shouldFallbackOnResult) {
return result;
}

const nextExecutor = remainingFallbackChain.shift();
if (nextExecutor === undefined) {
throw new FallbackChainExhaustedException();
}

executor = nextExecutor;

for (const onFallbackFn of this.onFallbackFns) {
try {
await onFallbackFn(result, undefined);
} catch (onFallbackError) {
// ignored
}
}

continue;
} catch (ex) {
if (ex instanceof FallbackChainExhaustedException) {
throw ex;
}

const shouldFallbackOnException = await this.isExceptionHandled(ex);
if (!shouldFallbackOnException) {
throw ex;
}

const nextExecutor = remainingFallbackChain.shift();
if (nextExecutor === undefined) {
throw new FallbackChainExhaustedException();
}

executor = nextExecutor;

for (const onFallbackFn of this.onFallbackFns) {
try {
await onFallbackFn(undefined, ex);
} catch (onFallbackError) {
// ignored
}
}

continue;
}
}
} finally {
try {
for (const onFinallyFn of this.onFinallyFns) {
try {
await onFinallyFn();
} catch (onFinallyError) {
// ignored
}
}
} finally {
this.executing--;
}
}
}
}
4 changes: 4 additions & 0 deletions src/policies/reactive/fallbackPolicy/onFallbackFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type OnFallbackFn<ResultType> = (
result: ResultType | undefined,
error: unknown | undefined,
) => void | Promise<void>;
1 change: 1 addition & 0 deletions src/policies/reactive/retryPolicy/backoffStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type BackoffStrategy = (currentRetryCount: number) => number | Promise<number>;
5 changes: 5 additions & 0 deletions src/policies/reactive/retryPolicy/onRetryFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type OnRetryFn<ResultType> = (
result: ResultType | undefined,
error: unknown | undefined,
currentRetryCount: number,
) => void | Promise<void>;
72 changes: 25 additions & 47 deletions src/policies/reactive/retryPolicy/retryPolicy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Predicate } from '../../../types/predicate';
import { OnFinallyFn } from '../../../types/onFinallyFn';
import { ReactivePolicy } from '../reactivePolicy';
import { BackoffStrategy } from './backoffStrategy';
import { OnRetryFn } from './onRetryFn';

export class RetryPolicy<ResultType> extends ReactivePolicy<ResultType> {
private totalRetryCount = 1;
private readonly onRetryFns: Array<
(result: ResultType | undefined, error: unknown | undefined, currentRetryCount: number) => void | Promise<void>
> = [];
private backoffStrategy: (currentRetryCount: number) => number | Promise<number> = (): number => 0;
private readonly onFinallyFns: Array<() => void | Promise<void>> = [];
private readonly onRetryFns: Array<OnRetryFn<ResultType>> = [];
private backoffStrategy: BackoffStrategy = (): number => 0;
private readonly onFinallyFns: OnFinallyFn[] = [];
private executing = 0;

public constructor() {
Expand Down Expand Up @@ -42,29 +42,23 @@ export class RetryPolicy<ResultType> extends ReactivePolicy<ResultType> {
this.totalRetryCount = Number.POSITIVE_INFINITY;
}

public onRetry(
fn: (
result: ResultType | undefined,
error: unknown | undefined,
currentRetryCount: number,
) => void | Promise<void>,
): void {
public onRetry(fn: OnRetryFn<ResultType>): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}

this.onRetryFns.push(fn);
}

public waitBeforeRetry(strategy: (currentRetryCount: number) => number | Promise<number>): void {
public waitBeforeRetry(strategy: BackoffStrategy): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}

this.backoffStrategy = strategy;
}

public onFinally(fn: () => void | Promise<void>): void {
public onFinally(fn: OnFinallyFn): void {
if (this.executing > 0) {
throw new Error('cannot modify policy during execution');
}
Expand All @@ -73,19 +67,23 @@ export class RetryPolicy<ResultType> extends ReactivePolicy<ResultType> {
}

public async execute(fn: () => ResultType | Promise<ResultType>): Promise<ResultType> {
this.executing++;

try {
this.executing++;

let currentRetryCount = 0;

// eslint-disable-next-line no-constant-condition
while (true) {
try {
const result = await fn();

const shouldRetryOnResult = await this.isResultHandled(result);
if (!shouldRetryOnResult) {
return result;
}

currentRetryCount++;
const shouldRetry = await this.shouldRetryOnResult(result, currentRetryCount);
if (!shouldRetry) {
if (!this.hasRetryLeft(currentRetryCount)) {
return result;
}

Expand All @@ -102,9 +100,13 @@ export class RetryPolicy<ResultType> extends ReactivePolicy<ResultType> {

continue;
} catch (ex) {
const shouldRetryOnException = await this.isExceptionHandled(ex);
if (!shouldRetryOnException) {
throw ex;
}

currentRetryCount++;
const shouldRetry = await this.shouldRetryOnException(ex, currentRetryCount);
if (!shouldRetry) {
if (!this.hasRetryLeft(currentRetryCount)) {
throw ex;
}

Expand Down Expand Up @@ -137,32 +139,8 @@ export class RetryPolicy<ResultType> extends ReactivePolicy<ResultType> {
}
}

private async shouldRetryOnResult(result: ResultType, currentRetryCount: number): Promise<boolean> {
return this.shouldRetryOn(
currentRetryCount,
result,
async (result): Promise<boolean> => this.isResultHandled(result),
);
}

private async shouldRetryOnException(exception: unknown, currentRetryCount: number): Promise<boolean> {
return this.shouldRetryOn(
currentRetryCount,
exception,
async (exception): Promise<boolean> => this.isExceptionHandled(exception),
);
}

private async shouldRetryOn<T>(
currentRetryCount: number,
subject: T,
shouldRetryCb: Predicate<T>,
): Promise<boolean> {
if (currentRetryCount > this.totalRetryCount) {
return false;
}

return shouldRetryCb(subject);
private hasRetryLeft(currentRetryCount: number): boolean {
return currentRetryCount <= this.totalRetryCount;
}

private async waitFor(ms: number): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions src/types/onFinallyFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type OnFinallyFn = () => void | Promise<void>;
Loading

0 comments on commit 11410d3

Please sign in to comment.