Skip to content

Commit

Permalink
feat: Implement TimeoutPolicy
Browse files Browse the repository at this point in the history
Closes #164.
  • Loading branch information
luczsoma committed Mar 15, 2020
1 parent 634628b commit 75bd2ee
Show file tree
Hide file tree
Showing 17 changed files with 1,101 additions and 513 deletions.
100 changes: 1 addition & 99 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,101 +1,3 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier/@typescript-eslint",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:promise/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint/eslint-plugin", "import", "promise"],
"rules": {
"no-console": ["error"],
"no-extra-parens": ["error", "all", { "enforceForArrowConditionals": false }],
"no-import-assign": ["error"],
"no-template-curly-in-string": ["error"],
"curly": ["error", "all"],
"eqeqeq": ["error"],
"max-classes-per-file": ["error", 1],
"no-alert": ["error"],
"no-caller": ["error"],
"no-div-regex": ["error"],
"no-else-return": ["error", { "allowElseIf": false }],
"no-eq-null": ["error"],
"no-eval": ["error"],
"no-extend-native": ["error"],
"no-extra-bind": ["error"],
"no-extra-label": ["error"],
"no-floating-decimal": ["error"],
"no-implicit-coercion": ["error"],
"no-implicit-globals": ["error"],
"no-implied-eval": ["error"],
"no-invalid-this": ["error"],
"no-iterator": ["error"],
"no-labels": ["error"],
"no-lone-blocks": ["error"],
"no-loop-func": ["error"],
"no-multi-spaces": ["error"],
"no-multi-str": ["error"],
"no-new": ["error"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-octal-escape": ["error"],
"no-param-reassign": ["error"],
"no-proto": ["error"],
"no-return-assign": ["error", "always"],
"no-return-await": ["error"],
"no-script-url": ["error"],
"no-self-compare": ["error"],
"no-sequences": ["error"],
"no-throw-literal": ["error"],
"no-unmodified-loop-condition": ["error"],
"no-useless-call": ["error"],
"no-useless-concat": ["error"],
"no-useless-return": ["error"],
"prefer-named-capture-group": ["error"],
"prefer-promise-reject-errors": ["error"],
"prefer-regex-literals": ["error"],
"radix": ["error", "always"],
"require-unicode-regexp": ["error"],
"wrap-iife": ["error", "inside"],
"yoda": ["error", "never"],
"array-bracket-spacing": ["error", "never"],
"brace-style": ["error"],
"camelcase": ["error"],
"func-call-spacing": ["error"],
"no-lonely-if": ["error"],
"no-nested-ternary": ["error"],
"no-new-object": ["error"],
"no-trailing-spaces": ["error"],
"semi-spacing": ["error", { "before": false, "after": true }],
"promise/prefer-await-to-then": ["error"],
"promise/prefer-await-to-callbacks": ["error"],
"import/first": ["error"],
"import/exports-last": ["error"],
"import/extensions": ["error"],
"import/newline-after-import": ["error"],
"import/no-unassigned-import": ["error"],
"import/no-named-default": ["error"],
"import/no-default-export": ["error"],
"@typescript-eslint/array-type": ["error", { "default": "array-simple" }],
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
"@typescript-eslint/explicit-function-return-type": ["error"],
"@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "explicit" }],
"@typescript-eslint/prefer-for-of": ["error"],
"@typescript-eslint/prefer-readonly": ["error"],
"@typescript-eslint/promise-function-async": ["error"],
"@typescript-eslint/restrict-plus-operands": ["error"],
"@typescript-eslint/strict-boolean-expressions": ["error"],
"@typescript-eslint/unified-signatures": ["error"]
}
"extends": ["@diplomatiq/eslint-config-tslib"]
}
109 changes: 107 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ Resily offers **reactive** and **proactive** policies:
| ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
| [**RetryPolicy**](#retrypolicy) | Many faults are transient and will not occur again after a delay. | Allows configuring automatic retries on specified conditions. |

#### Proactive policies

| 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. |

### Reactive policies

Every reactive policy extends the `ReactivePolicy` class, which means they can be configured with predicates to react on specific results and/or exceptions:
Expand Down Expand Up @@ -230,7 +236,7 @@ await policy.execute(async () => {

#### RetryPolicy

RetryPolicy claims that many faults are transient and will not occur again after a delay. It allows configuring automatic retries on specified conditions.
`RetryPolicy` claims that many faults are transient and will not occur again after a delay. It allows configuring automatic retries on specified conditions.

Configure how many retries you need or retry forever:

Expand Down Expand Up @@ -350,7 +356,7 @@ import { randomFill } from 'crypto';
export class NodeJsEntropyProvider implements EntropyProvider {
public async getRandomValues<T extends UnsignedTypedArray>(array: T): Promise<T> {
return new Promise<T>((resolve, reject): void => {
randomFill(array, (error: Error | null, array: T) => {
randomFill(array, (error: Error | null, array: T): void => {
if (error !== null) {
reject(error);
return;
Expand Down Expand Up @@ -398,6 +404,105 @@ policy.onFinally(() => {
});
```

### Proactive policies

Every proactive policy extends the `ProactivePolicy` class.

#### TimeoutPolicy

`TimeoutPolicy` claims that after some time, it is unlikely the call will be successful. It ensures the caller does not have to wait more than the specified timeout.

Only asynchronous methods can be executed within a `TimeoutPolicy`, or else no timeout happens. `TimeoutPolicy` is implemented with `Promise.race()`, racing the promise returned by the executed method (`executionPromise`) with a promise that is rejected after the specified time elapses (`timeoutPromise`). If the executed method is not asynchronous (i.e. it does not have at least one point to pause its execution at), no timeout will happen even if the execution takes longer than the specified timeout duration, since there is no point in time for taking the control out from the executed method's hands to reject the `timeoutPromise`.

The executed method is fully executed to its end (unless it throws an exception), regardless of whether a timeout has occured or not. `TimeoutPolicy` ensures that the caller does not have to wait more than the specified timeout, but it does neither cancel nor abort\* the execution of the method. This means that if the executed method has side effects, these side effects can occur even after the timeout happened.

\*TypeScript/JavaScript has no _generic_ way of canceling or aborting an executing method, either synchronous or asynchronous. `TimeoutPolicy` runs arbitrary user-provided code: it cannot be assumed the code is prepared in any way (e.g. it has cancel points). The provided code _could_ be executed in a separate worker thread so it can be aborted instantaneously by terminating the worker, but run-time compiling a worker from user-provided code is ugly and error-prone.

On timeout, the Promise returned by the policy's `execute` method is rejected with a `TimeoutException`:

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

const policy = new TimeoutPolicy();

try {
const result = await policy.execute(async () => {
// the executed code
});
} catch (ex) {
if (ex instanceof TimeoutException) {
// the operation timed out
} else {
// the executed method thrown an exception
}
}
```

Configure how long the waiting period should be:

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

const policy = new TimeoutPolicy();
policy.timeoutAfter(1000); // timeout after 1000 ms
```

Perform certain actions on timeout:

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

const policy = new TimeoutPolicy();
policy.onTimeout(
// onTimeoutFns can be sync or async, they will be awaited
async timedOutAfterMs => {
// the policy was configured to timeout after timedOutAfterMs
},
);

// you can set multiple onTimeoutFns, they will run sequentially
policy.onTimeout(async () => {
// this will be awaited first
});
policy.onTimeout(async () => {
// then this will be awaited
});

// errors thrown by an onTimeoutFn will be caught and ignored
policy.onTimeout(() => {
// throwing an error has no effect outside the method
throw new Error();
});
```

Throwing a `TimeoutException` from the executed method is not a timeout, therefore it does not trigger running `onTimeout` functions:

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

const policy = new TimeoutPolicy();

let onTimeoutRan = false;
policy.onTimeout(() => {
onTimeoutRan = true;
});

try {
await policy.execute(async () => {
throw new TimeoutException();
});
} catch (ex) {
// ex is a TimeoutException (thrown by the executed method)
const isTimeoutException = ex instanceof TimeoutException; // true
}

// onTimeoutRan is false
```

## Development

See [CONTRIBUTING.md](https://github.com/Diplomatiq/resily/blob/develop/CONTRIBUTING.md) for details.

---

Copyright (c) 2018 Diplomatiq
Loading

0 comments on commit 75bd2ee

Please sign in to comment.