Resilience Typescript
resilience-typescript
is a Typescript resilience and transient-fault-handling library that allows developers to add components like Timeout, Retry, Circuit Breaker, Cache, Token Cache to outgoing HTTP(S) calls, built on top of the Axios library with a fluent language. Primarly designed for backend service to service communication.
Installation
Run the following command:
npm i resilience-typescript
Quickstart
Basic CRUD Operations
You can create a resilient proxy that directly allows you to interact with a basic CRUD API server in a type save way with using e.g. a token cache, a circuit breaker, retry and timeout just like this:
export class Person {
public firstName: string;
public lastName: string;
public id: number;
}
const proxy = ResilientWebPipelineBuilder
.New()
.useConsoleLogger()
.useAzureAdTokenProvider(...)
.useCircuitBreaker(1, TimeSpansInMilliSeconds.tenMinutes, 10, TimeSpansInMilliSeconds.tenMinutes)
.useRetry(2, 10)
.useTimeout(3, TimeSpansInMilliSeconds.oneSecond)
.useBaseUrl("https://resilience-typescript.azurewebsites.net/api/persons")
.buildToCrud<Person>();
const list = await proxy.list();
const person: Person = { firstName: "Fabian", id: null, lastName: "Schwarz" };
const add = await proxy.add(person);
const get = await proxy.get("1");
person.firstName += " Resilience";
person.lastName += " Typescript";
const update = await proxy.update("1", person);
const delete = await proxy.delete("1");
Given the example above, the operations will call the following endpoints:
Customizable Fluent Resilient Requests
The previous builder allowed us to interact only with fixed endpoints of an API. To be more flexible and still have all the benefits of a resilient pipeline, all within a fluent, human readable language, you can build your pipeline to a request factory. This is the core purpose and heart of this library.
export class Person {
public firstName: string;
public lastName: string;
public id: number;
}
const proxy = ResilientWebPipelineBuilder
.New()
.useConsoleLogger()
.useAzureAdTokenProvider(...)
.useCircuitBreaker(1, TimeSpansInMilliSeconds.tenMinutes, 10, TimeSpansInMilliSeconds.tenMinutes)
.useRetry(2, 10)
.useTimeout(3, TimeSpansInMilliSeconds.oneSecond)
.builtToRequestFactory();
const list = await proxy
.request()
.get("https://my-api.com/api/persons")
.execute<Person[]>();
const item = await proxy
.request()
.get("https://another-api-for-persons.com/api/persons/4")
.execute<Person>();
const person: Person = { firstName: "Fabian", id: null, lastName: "Schwarz" };
const add = await proxy
.request()
.post("https://my-api.com/api/persons")
.withBody(person)
.execute<Person>();
Custom Axios Request Config
You can also create a pipeline where you can send custom Axios requets through using this example:
const proxy = ResilientWebPipelineBuilder
.New()
.useConsoleLogger()
.useAzureAdTokenProvider(...)
.useCircuitBreaker(1, TimeSpansInMilliSeconds.tenMinutes, 10, TimeSpansInMilliSeconds.tenMinutes)
.useRetry(2, 10)
.useTimeout(3, TimeSpansInMilliSeconds.oneSecond)
.build();
const request = { } as AxiosRequestConfig;
request.method = "GET";
request.url = "https://resilience-typescript.azurewebsites.net/api/persons";
const list = await proxy.execute<Person[]>(request);
Maintenance
Each of the builders above have a maintenace mode, where you can perform the following maintenance operations:
- Remove all entries of a memory cache
- Set the circuit breaker to a desired state
- Reset error count to zero for circuit breakers.
proxy.maintenance()
.cache()
.clear();
proxy.maintenance()
.circuitBreaker()
.resetErrorCount();
proxy.maintenance()
.circuitBreaker()
.close();
Components
Pipeline
A pipeline of so called resilience proxies (like Timeout, Retry and Circuit Breaker) that allows you to chain components together. You can build for instance a highly resilient pipeline were a Timeout is followed by a retry, whose is followed then by a Circuit Breaker.
const proxies: ResilienceProxy[] = [];
proxies.push(new CircuitBreakerProxy());
proxies.push(new RetryProxy());
proxies.push(new TimeoutProxy());
const pipeline = new PipelineProxy(proxies);
const func = async () => {...};
const result = await pipeline.execute(func);
Timeout
Checks if a promise resolves within a given timespan in ms. If not, it fails with a TimeoutError
.
const timeoutMs = 500;
const logger = new NoLogger();
const timeout = new TimeoutProxy(timeoutMs, new NoLogger());
const func = async () => {...};
try {
const result = await timeout.execute(func);
} catch (e) {
console.log(e.message);
}
Retry
Tries to execute a promise a serveral times. If the promise rejects every time, a RetryError
is thrown containing the error causing the promise to reject in the innerError
property.
const retries = 3;
const logger = new NoLogger();
const retry = new RetryProxy(retries, new NoLogger());
const func = async () => {...};
try {
const result = await retry.execute(func);
} catch (e) {
console.log(e.message);
}
Circuit Breaker
Allows a func to fail whithin a configurable timespan configurable times before failing fast on a subsequent func call. On failure a CircuitBreakerError
will be thrown with the innerError
property containing the real error.
const breakDuration = TimeSpansInMilliSeconds.oneMinute;
const maxFailedCalls = 5;
const logger = new NoLogger();
const leakTimeSpanInMilliSeconds = TimeSpansInMilliSeconds.tenMinutes;
const circuitBreaker = new CircuitBreakerProxy(breakDuration, maxFailedCalls, leakTimeSpanInMilliSeconds, logger, null);
const func = async () => {...};
try {
const result = await circuitBreaker.execute(func);
} catch (e) {
console.log(e.message);
}
Baseline
A proxy that calculates an alarm level from a list of func execution durations. Writes warnings to logs if execution duration is above alarm level. Additionaly writes for each func execution statistics of durations on information log level.
const startSamplingAfter = TimeSpansInMilliSeconds.tenMinutes;
const maxSampleDuration = TimeSpansInMilliSeconds.tenMinutes;
const maxSamplesCount = 100;
const logger = new NoLogger();
const baseLine = new BaselineProxy(startSamplingAfter, maxSampleDuration, maxSamplesCount, logger);
const func = async () => {...};
const result = await baseLine.execute(func);
Token Cache
A component that uses an implementation of the TokenProvider
interface to request a Bearer authorization token. As long as this token does not expire, it will be added automatically to all subsequent web calls Authorization
header. If the token expires, a new one will be automatically requested. A default implementation is available with the DefaultTokenCache
class.
TokenProvider
You can easily add your own authorization provider, by implementing the TokenProvider
provider where you request a token in any format and convert it to the general Token
class:
getToken(): Promise<Token>;
Azure Active Directory App Registration Token Provider
There's been already added a token provider for an Azure Active Directory App Registration. You'll need to provide the following information:
baseUrl
: Base URL for the token endpoint. Most of the time you'll be fine with https://login.microsoftonline.comclientId
: The GUID of your app registration. You can find this in the Azure portal.clientSecret
: A secret you've created for your app registration in the Azure portal.tenantId
: The GUID of your Azure Active Directory. You can find this also in the Azure portal.logger
: An implementation of the Logger<TState>
interface. You can find more information in the Logging section.
If no token can be retrieved whith the settings above, a TokenProviderError
will be thrown.
const baseUrl = "https://login.microsoftonline.com";
const clientId = "YOUR_CLIENT_ID";
const clientSecret = "YOUR_CLIENT_SECRET";
const tenantId = "YOUR_TENANT_ID";
const logger: Logger<string> = new NoLogger();
const provider: TokenProvider = new AzureActiveDirectoryAppRegistrationTokenProvider(baseUrl, clientId, clientSecret, tenantId, logger);
const result = await provider.getToken();
Logging
This package contains its own logging mechanism but you can easily include your own logger by extending the AbstractStringLogger
class, implementing the protected abstract logHandler(logLevel: LogLevel, guid: Guid, state: string, error: Error, formatter: (s: string, guid: Guid, e: Error) => string): void;
method and calling your own logger inside of it. The Guid is a unique Id for each request to be able to connect log messages to a specific request.
There are also three predefined logger already included:
ConsoleLogger
: A logger that writes every log message to the console.AppInsightsLogger
: A logger that writes every log messages to Azure Application Insights. Please ensure you have correctly setup Application Insights using this manual here before using this logger, otherwise you application will crash. All logs will be added as traces into Application Insights.MultiLogger
: A logger that is a container for other loggers. It will be used if you specify more than one logger in the builders, e.g. a ConsoleLogger and an AppInsightsLogger. The multi logger will then forward all messages to its two internal loggers.NoLogger
: A logger that does nothing, it basically disables all logging.TestLogger
: A logger that's primary designed for unit tests where you can provide a callback that will be called for each log message, to test if and what log messages are generated.
A example log ouput with a ConsoleLogger
and LogLevel.Information
where an endpoint returns three times a response code of 500 and succeeds on the fourth try looks like this:
IFORMATION: 2019-10-15 22:58:23.857 d94646ed-d988-91b3-dece-ed36144d5234 start GET 'https://resilience-typescript.azurewebsites.net/api/persons/6'
ERROR: 2019-10-15 22:58:24.167 d94646ed-d988-91b3-dece-ed36144d5234 Timeout failed with error "Request failed with status code 500"
WARNING: 2019-10-15 22:58:24.168 d94646ed-d988-91b3-dece-ed36144d5234 Retry 0/30 failed with error "Timeout failed"
ERROR: 2019-10-15 22:58:24.463 d94646ed-d988-91b3-dece-ed36144d5234 Timeout failed with error "Request failed with status code 500"
WARNING: 2019-10-15 22:58:24.501 d94646ed-d988-91b3-dece-ed36144d5234 Retry 1/30 failed with error "Timeout failed"
ERROR: 2019-10-15 22:58:24.830 d94646ed-d988-91b3-dece-ed36144d5234 Timeout failed with error "Request failed with status code 500"
WARNING: 2019-10-15 22:58:24.833 d94646ed-d988-91b3-dece-ed36144d5234 Retry 2/30 failed with error "Timeout failed"
INFORMATION: 2019-10-15 22:58:24.928 d94646ed-d988-91b3-dece-ed36144d5234 end 200 in 1070ms
Cache
An interface to provide a caching mechanism to not always query a depended service. A default implementation is provided with the MemoryCache
. You can easily create your own implementation by implementing the following interface:
export interface Cache<TKey, TResult> {
execute(func: (...args: any[]) => Promise<TResult>, key?: TKey): Promise<TResult>;
}
Memory Cache
A default implementation of a cache that stores all values in memory. To minimze memory usage, you can specify a sliding expiration of cache entries whereas garbage collection will take place on every configurable call to the cache. Also you can limit the maximum number of items in the cache.
const expirationTimeSpanMs = TimeSpansInMilliSeconds.oneHour;
const garbageCollectEveryXRequests = 100;
const maxEntryCount = 500;
const key = "KeyForFunc";
const func = async () => {...};
const cache = new MemoryCache<string>(expirationTimeSpanMs, logger, garbageCollectEveryXRequests, maxEntryCount);
try {
const result = await cache.execute(func, key);
} catch (e) {
console.log(e.message);
}
Queue
An interface to provide a queue in TypeScript, it is used in the MemoryCache
to limit the maximum count of entries in the cache. A default implementation is provided with the MemoryQueue
.
export interface Queue<T> {
readonly maxLength: number;
push(value: string): QueuePushResult<T>;
pop(): T | undefined;
}
Memory Queue
A simple default implementation of a queue to limit the maximum count entries in the MemoryCache
.
const maxLength = 100;
const queue = new MemoryQueue(maxLength, logger);
const result = queue.push("First");
const hasPoped = result.hasPoped;
const popedItem = result.popedItem;
Changelog
The complete changlog can be found in the CHANGELOG.md file
Code of conduct
Discover the code of conduct in the CODE_OF_CONDUCT.md file
Contributing
The contributing guidelines can be read in the CONTRIBUTING.md file
License
Unless stated otherwise all works are:
Copyright © 2019+ Fabian Schwarz
and licensed under:
MIT License
Buy me a coffee
If you like this package you can buy me a coffee: