Skip to content

Commit

Permalink
feat: Implement PolicyCombination
Browse files Browse the repository at this point in the history
Closes #209.
  • Loading branch information
luczsoma committed Apr 2, 2020
1 parent 987f888 commit 84e63aa
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 17 deletions.
59 changes: 53 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,18 @@ Resily offers **reactive** and **proactive** policies:

#### Proactive policies summary

| Policy | What does it claim? | How does it work? |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| [**TimeoutPolicy**](#timeoutpolicy) | After some time, it is unlikely that the call will be successful. | Ensures the caller does not have to wait more than the specified timeout. |
| [**BulkheadIsolationPolicy**](#bulkheadisolationpolicy) | Too many concurrent calls can overload a resource. | Limits the number of concurrently executed actions as specified. |
| [**CachePolicy**](#cachepolicy) | Within a given time frame, a system may respond with the same answer, thus there is no need to actually perform the query. | Retrieves the response from a local cache within the time frame, after storing it on the first query. |
| [**NopPolicy**](#noppolicy) | Does not claim anything. | Executes the wrapped method, and returns its result or throws its exceptions, without any intervention. |
| Policy | What does it claim? | How does it work? |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [**TimeoutPolicy**](#timeoutpolicy) | After some time, it is unlikely that the call will be successful. | Ensures the caller does not have to wait more than the specified timeout. |
| [**BulkheadIsolationPolicy**](#bulkheadisolationpolicy) | Too many concurrent calls can overload a resource. | Limits the number of concurrently executed actions as specified. |
| [**CachePolicy**](#cachepolicy) | Within a given time frame, a system may respond with the same answer, thus there is no need to actually perform the query. | Retrieves the response from a local cache within the time frame, after storing it on the first query. |

#### Helpers and utilities summary

| Policy | What does it claim? | How does it work? |
| ------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| [**NopPolicy**](#noppolicy) | Does not claim anything. | Executes the wrapped method, and returns its result or throws its exceptions, without any intervention. |
| [**PolicyCombination**](#policycombination) | Combining policies leads to better resilience. | Allows any policies to be combined together. |

### Reactive policies

Expand Down Expand Up @@ -992,10 +998,51 @@ policy.onCachePut(() => {
});
```

### Helpers and utilities

#### NopPolicy

`NopPolicy` does not claim anything. It executes the wrapped method, and returns its result or throws its exceptions, without any intervention.

#### PolicyCombination

Policies can be combined in multiple ways. Given the following:

```typescript
import { FallbackPolicy, RetryPolicy, TimeoutPolicy } from '@diplomatiq/resily';

const timeoutPolicy = new TimeoutPolicy();
const retryPolicy = new RetryPolicy();
const fallbackPolicy = new FallbackPolicy();

const fn = () => {
// the executed code
};
```

The naïve way to combine the above policies would be:

```typescript
fallbackPolicy.execute(() => retryPolicy.execute(() => timeoutPolicy.execute(fn)));
```

The previous example is equivalent to the following:

```typescript
fallbackPolicy.wrap(retryPolicy);
retryPolicy.wrap(timeoutPolicy);

fallbackPolicy.execute(fn);
```

And also equivalent to the following:

```typescript
PolicyCombination.wrap([fallbackPolicy, retryPolicy, timeoutPolicy]).execute(fn);
```

PolicyCombination expects at least two policies to be combined.

### Modifying a policy's configuration

All policies' configuration parameters are set via setter methods. This could imply that all policies can be safely reconfigured whenever needed, but providing setter methods instead of constructor parameters is merely because this way the policies are more convenient to use. If you need to reconfigure a policy, you can do that, but not while it is still executing one or more methods: reconfiguring while executing could lead to unexpected side-effects. Therefore, if you tries to reconfigure a policy while executing, a `PolicyModificationNotAllowedException` is thrown.
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export { RandomGenerator } from './interfaces/randomGenerator';
export { NopPolicy } from './policies/nopPolicy';
export { Policy } from './policies/policy';
export { PolicyCombination } from './policies/policyCombination';
export { BulkheadCompartmentRejectedException } from './policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException';
export { BulkheadIsolationPolicy } from './policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy';
export { CachePolicy } from './policies/proactive/cachePolicy/cachePolicy';
export { OnCacheGetFn } from './policies/proactive/cachePolicy/onCacheGetFn';
export { OnCacheMissFn } from './policies/proactive/cachePolicy/onCacheMissFn';
export { OnCachePutFn } from './policies/proactive/cachePolicy/onCachePutFn';
export { TimeToLiveStrategy } from './policies/proactive/cachePolicy/timeToLiveStrategy';
export { NopPolicy } from './policies/proactive/nopPolicy/nopPolicy';
export { ProactivePolicy } from './policies/proactive/proactivePolicy';
export { ExecutionException } from './policies/proactive/timeoutPolicy/executionException';
export { OnTimeoutFn } from './policies/proactive/timeoutPolicy/onTimeoutFn';
Expand Down
8 changes: 8 additions & 0 deletions src/policies/nopPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ExecutedFn } from '../types/executedFn';
import { Policy } from './policy';

export class NopPolicy<ResultType> extends Policy<ResultType> {
protected async policyExecutorImpl(fn: ExecutedFn<ResultType>): Promise<ResultType> {
return fn();
}
}
18 changes: 17 additions & 1 deletion src/policies/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import { PolicyModificationNotAllowedException } from '../types/policyModificati

export abstract class Policy<ResultType> {
private executing = 0;
private wrappedPolicy?: Policy<ResultType>;

public async execute(fn: ExecutedFn<ResultType>): Promise<ResultType> {
try {
this.executing++;
return await this.policyExecutorImpl(fn);

return await this.policyExecutorImpl(
async (): Promise<ResultType> => {
if (this.wrappedPolicy !== undefined) {
return this.wrappedPolicy.execute(fn);
}

return fn();
},
);
} finally {
this.executing--;
}
Expand All @@ -17,6 +27,12 @@ export abstract class Policy<ResultType> {
return this.executing > 0;
}

public wrap(policy: Policy<ResultType>): void {
this.throwForPolicyModificationIfExecuting();

this.wrappedPolicy = policy;
}

protected throwForPolicyModificationIfExecuting(): void {
if (this.isExecuting()) {
throw new PolicyModificationNotAllowedException();
Expand Down
14 changes: 14 additions & 0 deletions src/policies/policyCombination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Policy } from './policy';

export class PolicyCombination {
public static combine<ResultType>(
policies: [Policy<ResultType>, Policy<ResultType>, ...Array<Policy<ResultType>>],
): Policy<ResultType> {
return policies.reduceRight(
(prev, next): Policy<ResultType> => {
next.wrap(prev);
return next;
},
);
}
}
8 changes: 0 additions & 8 deletions src/policies/proactive/nopPolicy/nopPolicy.ts

This file was deleted.

2 changes: 1 addition & 1 deletion test/specs/nopPolicy.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { NopPolicy } from '../../src/policies/proactive/nopPolicy/nopPolicy';
import { NopPolicy } from '../../src/policies/nopPolicy';

describe('NopPolicy', (): void => {
it('should run the synchronous execution callback and return its result', async (): Promise<void> => {
Expand Down
122 changes: 122 additions & 0 deletions test/specs/policyCombination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { expect } from 'chai';
import { PolicyCombination } from '../../src/policies/policyCombination';
import { TimeoutException } from '../../src/policies/proactive/timeoutPolicy/timeoutException';
import { TimeoutPolicy } from '../../src/policies/proactive/timeoutPolicy/timeoutPolicy';
import { FallbackPolicy } from '../../src/policies/reactive/fallbackPolicy/fallbackPolicy';
import { RetryPolicy } from '../../src/policies/reactive/retryPolicy/retryPolicy';

describe('PolicyCombination', (): void => {
it('should run the wrapped policy inside the wrapper policy (wrapped with singular wrapping)', async (): Promise<
void
> => {
let onRetryExecuted = 0;
let onFallbackExecuted = 0;

const fallbackPolicy = new FallbackPolicy<boolean>();
fallbackPolicy.reactOnResult((r): boolean => r);
fallbackPolicy.fallback((): boolean => {
return false;
});
fallbackPolicy.onFallback((): void => {
expect(onRetryExecuted).to.equal(1);
onFallbackExecuted++;
});

const retryPolicy = new RetryPolicy<boolean>();
retryPolicy.reactOnResult((r): boolean => r);
retryPolicy.onRetry((): void => {
expect(onFallbackExecuted).to.equal(0);
onRetryExecuted++;
});

fallbackPolicy.wrap(retryPolicy);

await fallbackPolicy.execute((): boolean => {
return true;
});

expect(onRetryExecuted).to.equal(1);
expect(onFallbackExecuted).to.equal(1);
});

it('should run the wrapped policy inside the wrapper policy (combined with PolicyCombination)', async (): Promise<
void
> => {
let onRetryExecuted = 0;
let onFallbackExecuted = 0;

const fallbackPolicy = new FallbackPolicy<boolean>();
fallbackPolicy.reactOnResult((r): boolean => r);
fallbackPolicy.fallback((): boolean => {
return false;
});
fallbackPolicy.onFallback((): void => {
expect(onRetryExecuted).to.equal(1);
onFallbackExecuted++;
});

const retryPolicy = new RetryPolicy<boolean>();
retryPolicy.reactOnResult((r): boolean => r);
retryPolicy.onRetry((): void => {
expect(onFallbackExecuted).to.equal(0);
onRetryExecuted++;
});

const wrappedPolicy = PolicyCombination.combine<boolean>([fallbackPolicy, retryPolicy]);
await wrappedPolicy.execute((): boolean => {
return true;
});

expect(onRetryExecuted).to.equal(1);
expect(onFallbackExecuted).to.equal(1);
});

it('should construct a policy which combines the other policies sequentially', async (): Promise<void> => {
let onTimeoutExecuted = 0;
let onRetryExecuted = 0;
let onFallbackExecuted = 0;

const fallbackPolicy = new FallbackPolicy<void>();
fallbackPolicy.reactOnException((e): boolean => e instanceof TimeoutException);
fallbackPolicy.fallback((): void => {
// empty
});
fallbackPolicy.onFallback((): void => {
expect(onTimeoutExecuted).to.equal(1);
expect(onRetryExecuted).to.equal(1);
expect(onFallbackExecuted).to.equal(0);
onFallbackExecuted++;
});

const retryPolicy = new RetryPolicy<void>();
retryPolicy.reactOnException((e): boolean => e instanceof TimeoutException);
retryPolicy.onRetry((): void => {
expect(onTimeoutExecuted).to.equal(1);
expect(onRetryExecuted).to.equal(0);
expect(onFallbackExecuted).to.equal(0);
onRetryExecuted++;
});

const timeoutPolicy = new TimeoutPolicy<void>();
timeoutPolicy.timeoutAfter(1);
timeoutPolicy.onTimeout((): void => {
expect(onTimeoutExecuted).to.equal(0);
expect(onRetryExecuted).to.equal(0);
expect(onFallbackExecuted).to.equal(0);
onTimeoutExecuted++;
});

const wrappedPolicies = PolicyCombination.combine<void>([fallbackPolicy, retryPolicy, timeoutPolicy]);
await wrappedPolicies.execute(
async (): Promise<void> => {
return new Promise((resolve): void => {
setTimeout(resolve, 5);
});
},
);

expect(onTimeoutExecuted).to.equal(1);
expect(onRetryExecuted).to.equal(1);
expect(onFallbackExecuted).to.equal(1);
});
});

0 comments on commit 84e63aa

Please sign in to comment.