Skip to content

Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Fallback, Circuit Breaker, Timeout, Bulkhead Isolation, and Cache. Inspired by App-vNext/Polly.

License

Notifications You must be signed in to change notification settings

Diplomatiq/resily

Repository files navigation

Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Fallback, Circuit Breaker, Timeout, Bulkhead Isolation, and Cache. Inspired by App-vNext/Polly.

build status languages used downloads from npm latest released version on npm license


Installation

Being an npm package, you can install resily with the following command:

npm install -P @diplomatiq/resily

Testing

Run tests with the following:

npm test

Usage

Note: This package is built as an ES6 package. You will not be able to use require().

After installation, you can import policies and other helper classes into your project, then wrap your code into one or more policies.

Every policy extends the abstract Policy class, which has an execute method. Your code wrapped into a policy gets executed when you invoke execute. The execute method is asynchronous, so it returns a Promise resolving with the return value of the executed method (or rejecting with an exception thrown by the method).

The wrapped method can be synchronous or asynchronous, it will be awaited in either case:

async function main() {
    const policy =  // any policy

    // configure the policy before executing code, see below

    // then execute some code wrapped into the policy
    // execute is async, so it returns a Promise
    const result = await policy.execute(
        // the wrapped method can be sync or async
        async () => {
            // the executed code
            return 5;
        },
    );

    // the value of result is 5
}

See concrete usage examples below at the policies' documentation.

Policies

Resily offers reactive and proactive policies:

  • A reactive policy executes the wrapped method, then reacts to the outcome (which in practice is the result of or an exception thrown by the executed method) by acting as specified in the policy itself. Examples for reactive policies include retry, fallback, circuit-breaker.
  • A proactive policy executes the wrapped method, then acts on its own as specified in the policy itself, regardless of the outcome of the executed code. Examples for proactive policies include timeout, bulkhead isolation, cache.

Reactive policies summary

Policy What does it claim? How does it work?
RetryPolicy Many faults are transient and will not occur again after a delay. Allows configuring automatic retries on specified conditions.
FallbackPolicy Failures happen, and we can prepare for them. Allows configuring substitute values or automated fallback actions.
CircuitBreakerPolicy Systems faulting under heavy load can recover easier without even more load — in these cases it's better to fail fast than to keep callers on hold for a long time. If there are more consecutive faulty responses than the configured number, it breaks the circuit (blocks the executions) for a specified time period.

Proactive policies summary

Policy What does it claim? How does it work?
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 Too many concurrent calls can overload a resource. Limits the number of concurrently executed actions as specified.
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 Does not claim anything. Executes the wrapped method, and returns its result or throws its exceptions, without any intervention.
PolicyCombination Combining policies leads to better resilience. Allows any policies to be combined together.

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:

const policy =  // any reactive policy

// if the executed code returns 5, the policy will react
policy.reactOnResult(r => r === 5);

// will react
await policy.execute(() => 5);

// will react
await policy.execute(async () => 5);

// will not react
await policy.execute(() => 2);

// will not react
await policy.execute(async () => 2);
const policy =  // any reactive policy

// if the executed code throws a ConcurrentAccessException, the policy will react
policy.reactOnException(e => e instanceof ConcurrentAccessException);

// will react
await policy.execute(() => {
    throw new ConcurrentAccessException();
});

// will react
await policy.execute(async () => {
    throw new ConcurrentAccessException();
});

// will not react
await policy.execute(() => {
    throw new OutOfRangeException();
});

// will not react
await policy.execute(async () => {
    throw new OutOfRangeException();
});

If the policy is configured to react on multiple kinds of results or exceptions, it will react if any of them occurs:

const policy =  // any reactive policy

policy.reactOnResult(r => r === 5);
policy.reactOnResult(r => r === 7);
policy.reactOnException(e => e instanceof ConcurrentAccessException);
policy.reactOnException(e => e instanceof InvalidArgumentException);

// will react
await policy.execute(() => 5);

// will react
await policy.execute(async () => 5);

// will react
await policy.execute(() => 7);

// will react
await policy.execute(async () => 7);

// will react
await policy.execute(() => {
    throw new ConcurrentAccessException();
});

// will react
await policy.execute(async () => {
    throw new ConcurrentAccessException();
});

// will react
await policy.execute(() => {
    throw new InvalidArgumentException();
});

// will react
await policy.execute(async () => {
    throw new InvalidArgumentException();
});

// will not react
await policy.execute(() => 2);

// will not react
await policy.execute(async () => 2);

// will not react
await policy.execute(() => {
    throw new OutOfRangeException();
});

// will not react
await policy.execute(async () => {
    throw new OutOfRangeException();
});

You can configure the policy to react on any result and/or to any exception:

const policy =  // any reactive policy

// react on any result
policy.reactOnResult(() => true);

// react on any exception
policy.reactOnException(() => true);

RetryPolicy

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

Since RetryPolicy is a reactive policy, you need to configure the policy to retry the execution on specific results or exceptions with reactOnResult and reactOnException. See the Reactive policies section for details.

Configure how many retries you need or retry forever:

import { RetryPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new RetryPolicy<string>();

// retry until the result/exception is reactive, but maximum 3 times
policy.retryCount(3);

// this overwrites the previous value
policy.retryCount(5);

// this also overwrites the previous value
// this is the same as policy.retryCount(Number.POSITIVE_INFINITY)
policy.retryForever();

Perform certain actions before retrying:

import { RetryPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new RetryPolicy<string>();

policy.onRetry(
    // onRetryFns can be sync or async, they will be awaited
    async (result, error, currentRetryCount) => {
        // this code will be executed before the currentRetryCount-th retry occurs
        // result is undefined if reacting upon a thrown error
        // error is undefined if reacting upon a result
    },
);

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

Wait for the specified number of milliseconds before retrying:

import { RetryPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new RetryPolicy<string>();

// wait for 100 ms before each retry
policy.waitBeforeRetry(() => 100);

// this overwrites the previous backoff strategy
// wait for 100 ms before the first retry, 200 ms before the second retry, etc.
policy.waitBeforeRetry((currentRetryCount) => currentRetryCount * 100);

The waiting happens before the execution of onRetryFns.

Although you can code any kind of backoff, there are also predefined, ready-to-use backoff strategies:

import { BackoffStrategyFactory, RetryPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new RetryPolicy<string>();

// wait for 100 ms before each retry
// 100 100 100 100 100 …
policy.waitBeforeRetry(BackoffStrategyFactory.constantBackoff(100));

// retry immediately for the first time, then wait for 100 ms before each retry
// 0 100 100 100 100 …
policy.waitBeforeRetry(BackoffStrategyFactory.constantBackoff(100, true));

// wait for (currentRetryCount * 100) ms before each retry
// 100 200 300 400 500 …
policy.waitBeforeRetry(BackoffStrategyFactory.linearBackoff(100));

// retry immediately for the first time, then wait for ((currentRetryCount - 1) * 100) ms before each retry
// 0 100 200 300 400 …
policy.waitBeforeRetry(BackoffStrategyFactory.linearBackoff(100, true));

// wait for (100 * 2 ** (currentRetryCount - 1)) ms before each retry
// 100 200 400 800 1600 …
policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100));

// retry immediately for the first time, then wait for (100 * 2 ** (currentRetryCount - 2)) ms before each retry
// 0 100 200 400 800 …
policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, true));

// wait for (100 * 3 ** (currentRetryCount - 1)) ms before each retry
// 100 300 900 2700 8100 …
policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, false, 3));

// retry immediately for the first time, then wait for (100 * 3 ** (currentRetryCount - 2)) ms before each retry
// 0 100 300 900 2700 …
policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, true, 3));

// wait for a [random between 1-100, inclusive] ms before each retry
policy.waitBeforeRetry(BackoffStrategyFactory.jitteredBackoff(1, 100));

// retry immediately for the first time, then wait for a [random between 1-100, inclusive] ms before each retry
policy.waitBeforeRetry(BackoffStrategyFactory.jitteredBackoff(1, 100, true));

For using jitteredBackoff in Node.js environments, you will need to inject a Node.js-based entropy source into the default RandomGenerator (@diplomatiq/crypto-random requires window.crypto.getRandomValues to be available by default). Create the following in your project:

import { EntropyProvider, UnsignedTypedArray } from '@diplomatiq/crypto-random';
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): void => {
                if (error !== null) {
                    reject(error);
                    return;
                }
                resolve(array);
            });
        });
    }
}

Then use it as follows:

import { RandomGenerator } from '@diplomatiq/crypto-random';
import { NodeJsEntropyProvider } from './nodeJsEntropyProvider';

const entropyProvider = new NodeJsEntropyProvider();
const randomGenerator = new RandomGenerator(entropyProvider);

const jitteredBackoff = BackoffStrategyFactory.jitteredBackoff(1, 100, true, randomGenerator);

Perform certain actions after the execution and all retries finished:

import { RetryPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new RetryPolicy<string>();

policy.onFinally(
    // onFinallyFns can be sync or async, they will be awaited
    async () => {},
);

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

FallbackPolicy

FallbackPolicy claims that failures happen, and we can prepare for them. It allows configuring substitute values or automated fallback actions.

Since FallbackPolicy is a reactive policy, you need to configure the policy to fallback along its fallback chain on specific results or exceptions with reactOnResult and reactOnException. See the Reactive policies section for details.

Configure the fallback chain:

import { FallbackPolicy } from '@diplomatiq/resily';

// the wrapped method and its fallbacks are supposed to return a string
const policy = new FallbackPolicy<string>();

// if the wrapped method's result/exception is reactive, configure a fallback method onto the fallback chain
policy.fallback(
    // the fallback methods can be sync or async, they will be awaited
    () => {
        // do something
    },
);

// if the previous fallback method's result/exception is reactive, configure another fallback onto the fallback chain
policy.fallback(
    // the fallback methods can be sync or async, they will be awaited
    async () => {
        // do something
    },
);

// you can configure any number of fallback methods onto the fallback chain

If there are no more elements on the fallback chain but the last result/exception is still reactive — meaning there are no more fallbacks when needed —, a FallbackChainExhaustedException is thrown.

Perform certain actions before the fallback:

import { FallbackPolicy } from '@diplomatiq/resily';

// the wrapped method and its fallbacks are supposed to return a string
const policy = new FallbackPolicy<string>();

policy.onFallback(
    // onFallbackFns can be sync or async, they will be awaited
    async (result, error) => {
        // result is undefined if reacting upon a thrown error
        // error is undefined if reacting upon a result
    },
);

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

Perform certain actions after the execution and all fallbacks finished:

import { FallbackPolicy } from '@diplomatiq/resily';

// the wrapped method and its fallbacks are supposed to return a string
const policy = new FallbackPolicy<string>();

policy.onFinally(
    // onFinallyFns can be sync or async, they will be awaited
    async () => {},
);

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

CircuitBreakerPolicy

CircuitBreakerPolicy claims that systems faulting under heavy load can recover easier without even more load — in these cases it's better to fail fast than to keep callers on hold for a long time.

If there are more consecutive faulty responses than the configured number, it breaks the circuit (blocks the executions) for a specified time period.

Since CircuitBreakerPolicy is a reactive policy, you need to configure the policy to break the circuit on specific results or exceptions with reactOnResult and reactOnException. See the Reactive policies section for details.

The CircuitBreakerPolicy has 4 states, and works as follows:

Closed

  • This is the initial state.
  • When closed, the circuit allows executions, while measuring reactive results and exceptions. All results (reactive or not) are returned and all exceptions (reactive or not) are rethrown.
  • When encountering altogether numberOfConsecutiveReactionsBeforeCircuitBreak reactive results or exceptions consecutively, the circuit transitions to Open state, meaning the circuit is broken.

Open

  • While the circuit is in Open state, no action wrapped into the policy gets executed. Every call will fail fast with a BrokenCircuitException.
  • The circuit remains open for the specified duration. After the duration elapses, the subsequent execution call transitions the circuit to AttemptingClose state.

AttemptingClose

  • As the name implies, this state is an attempt to close the circuit.

  • This is a temporary state of the circuit, existing only between the subsequent execution call to the circuit after the break duration elapsed in Open state, and the actual execution of the wrapped method.

  • The next circuit state is determined by the result or exception produced by the executed method.

    • If the result or exception is reactive to the policy, the circuit transitions back to Open state for the specified circuit break duration.
    • If the result or exception is not reactive to the policy, the circuit transitions to Closed state.

Isolated

  • You can manually break the circuit by calling policy.isolate(), from any state. This transitions the circuit to Isolated state.
  • While the circuit is in Isolated state, no action wrapped into the policy gets executed. Every call will fail fast with an IsolatedCircuitException.
  • The circuit remains in Isolated state until policy.reset() is called.

Configure how many consecutive reactions should break the circuit:

import { CircuitBreakerPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CircuitBreakerPolicy<string>();

// break the circuit after encountering 3 reactive results/exceptions consecutively
policy.breakAfter(3);

// this overwrites the previous value
policy.breakAfter(5);

Configure how long the circuit should be broken:

import { CircuitBreakerPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CircuitBreakerPolicy<string>();

// break the circuit for 5000 ms
policy.breakFor(5000);

// this overwrites the previous value
policy.breakFor(20000);

Manage the circuit manually:

import { CircuitBreakerPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CircuitBreakerPolicy<string>();

// break the circuit manually - it will be open indefinitely
await policy.isolate();

// get the circuit's current state
const state = policy.getCircuitState();
// 'Closed' | 'Open' | 'AttemptingClose' | 'Isolated'

// reset the circuit after isolating - it will close
if (state === 'Isolated') {
    await policy.reset();
}

Perform actions on state transitions:

import { CircuitBreakerPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CircuitBreakerPolicy<string>();
policy.onClose(
    // onCloseFns can be sync or async, they will be awaited
    async () => {},
);

// you can set multiple onCloseFns, they will run sequentially
policy.onClose(async () => {
    // this will be awaited first
});
policy.onClose(async () => {
    // then this will be awaited
});
policy.onOpen(
    // onOpenFns can be sync or async, they will be awaited
    async () => {},
);

// you can set multiple onOpenFns, they will run sequentially
policy.onOpen(async () => {
    // this will be awaited first
});
policy.onOpen(async () => {
    // then this will be awaited
});
policy.onAttemptingClose(
    // onAttemptingCloseFns can be sync or async, they will be awaited
    async () => {},
);

// you can set multiple onAttemptingCloseFns, they will run sequentially
policy.onAttemptingClose(async () => {
    // this will be awaited first
});
policy.onAttemptingClose(async () => {
    // then this will be awaited
});
policy.onIsolate(
    // onIsolateFns can be sync or async, they will be awaited
    async () => {},
);

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

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:

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

// the wrapped method is supposed to return a string
const policy = new TimeoutPolicy<string>();

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:

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

// the wrapped method is supposed to return a string
const policy = new TimeoutPolicy<string>();
policy.timeoutAfter(1000); // timeout after 1000 ms

Perform certain actions on timeout:

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

// the wrapped method is supposed to return a string
const policy = new TimeoutPolicy<string>();
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
});

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

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

// the wrapped method is supposed to return a string
const policy = new TimeoutPolicy<string>();

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

BulkheadIsolationPolicy

BulkheadIsolationPolicy claims that too many concurrent calls can overload a resource. It limits the number of concurrently executed actions as specified.

Method calls executed via the policy are placed into a size-limited bulkhead compartment, limiting the maximum number of concurrent executions.

If the bulkhead compartment is full — meaning the maximum number of concurrent executions is reached —, additional calls can be queued up, ready to be executed whenever a place falls vacant in the bulkhead compartment (i.e. an execution finishes). Queuing up these calls ensures that the resource protected by the policy is always at maximum utilization, while limiting the number of concurrent actions ensures that the resource is not overloaded. The queue is a simple FIFO buffer.

When the policy's execute method is invoked with a method to be executed, the policy's operation can be described as follows:

  • (1) If there is an execution slot available in the bulkhead compartment, execute the method immediately.

  • (2) Else if there is still space in the queue, enqueue the execution intent of the method — without actually executing the method —, then wait asynchronously until the method can be executed.

    An execution intent gets dequeued — and its corresponding method gets executed — each time an execution slot becomes available in the bulkhead compartment.

  • (3) Else throw a BulkheadCompartmentRejectedException.

From the caller's point of view, this is all transparent: the promise returned by the execute method is

  • either eventually resolved with the return value of the wrapped method (cases (1) and (2)),
  • or eventually rejected with an exception thrown by the wrapped method (cases (1) and (2)),
  • or rejected with a BulkheadCompartmentRejectedException (case (3)).

Configure the size of the bulkhead compartment:

import { BulkheadIsolationPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new BulkheadIsolationPolicy<string>();

// allow maximum 3 concurrent executions
policy.maxConcurrency(3);

// this overwrites the previous value
policy.maxConcurrency(5);

Configure the size of the queue:

import { BulkheadIsolationPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new BulkheadIsolationPolicy<string>();

// allow maximum 3 queued actions
policy.maxQueuedActions(3);

// this overwrites the previous value
policy.maxQueuedActions(5);

Get usage information about the bulkhead compartment:

import { BulkheadIsolationPolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new BulkheadIsolationPolicy<string>();

// the number of available (free) execution slots in the bulkhead compartment
policy.getAvailableSlotsCount();

// the number of available (free) spaces in the queue
policy.getAvailableQueuedActionsCount();

CachePolicy

CachePolicy claims that within a given time frame, a system may respond with the same answer, thus there is no need to actually perform the query. It retrieves the response from a local cache within the time frame, after storing it on the first query.

The CachePolicy is implemented as a simple in-memory cache. It works as follows:

  • For the first time (and every further time the cache is invalid), the CachePolicy executes the wrapped method, and caches its result.
  • For subsequent execution calls, the cached result is returned and the wrapped method is not executed — as long as the cache remains valid.
  • The cache is valid as long as it is not expired (see time to live settings below) or manually invalidated.

Configure how long the cache should be valid:

import { CachePolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CachePolicy<string>;

// the cache is valid for 10000ms from the moment the value is stored in the cache
policy.timeToLive('relative', 10000);

// the cache is valid as long as Date.now() < 772149600000
// this overwrites the previous setting
policy.timeToLive('absolute', 772149600000);

// the cache is valid for 10000ms from the moment the value is stored in or retrieved from the cache
// this overwrites the previous setting
policy.timeToLive('sliding', 10000);

Invalidate the cache manually, causing the next execute call to run the wrapped method:

import { CachePolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CachePolicy<string>;

policy.invalidate();

Perform actions on caching events:

import { CachePolicy } from '@diplomatiq/resily';

// the wrapped method is supposed to return a string
const policy = new CachePolicy<string>;
// perform an action before the value is retrieved from the cache
policy.onCacheGet(
    // onCacheGetFns can be sync or async, they will be awaited
    async () => {},
);

// you can set multiple onCacheGetFns, they will run sequentially
policy.onCacheGet(async () => {
    // this will be awaited first
});
policy.onCacheGet(async () => {
    // then this will be awaited
});
// perform an action before the wrapped method is executed and its result is cached
policy.onCacheMiss(
    // onCacheMissFns can be sync or async, they will be awaited
    async () => {},
);

// you can set multiple onCacheMissFns, they will run sequentially
policy.onCacheMiss(async () => {
    // this will be awaited first
});
policy.onCacheMiss(async () => {
    // then this will be awaited
});
// perform an action after the wrapped method is executed and its result is cached
policy.onCachePut(
    // onCachePutFns can be sync or async, they will be awaited
    async () => {},
);

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

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:

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:

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

The previous example is equivalent to the following:

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

fallbackPolicy.execute(fn);

And also equivalent to the following:

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.

To safely reconfigure a policy, check whether it is executing or not:

const policy =  // any policy

if (!policy.isExecuting()) {
    // you can reconfigure the policy
}

Development

See CONTRIBUTING.md for details.


Copyright (c) 2018 Diplomatiq

About

Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Fallback, Circuit Breaker, Timeout, Bulkhead Isolation, and Cache. Inspired by App-vNext/Polly.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •