Cockatiel is resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback. .NET has Polly, a wonderful one-stop shop for all your fault handling needs--I missed having such a library for my JavaScript projects, and grew tired of copy-pasting retry logic between my projects. Hence, this module!
npm install --save cockatiel
Then go forth with confidence:
// alternatively: const { Policy, ConsecutiveBreaker } = require('cockatiel');
import { Policy, ConsecutiveBreaker } from 'cockatiel';
import { database } from './my-db';
// Create a retry policy that'll try whatever function we execute 3
// times with a randomized exponential backoff.
const retry = Policy.handleAll().retry().attempts(3).exponential();
// Create a circuit breaker that'll stop calling the executed function for 10
// seconds if it fails 5 times in a row. This can give time for e.g. a database
// to recover without getting tons of traffic.
const circuitBreaker = Policy.handleAll().circuitBreaker(10 * 1000, new ConsecutiveBreaker(5));
// Combine these! Create a policy that retries 3 times, calling through the circuit breaker
const retryWithBreaker = Policy.wrap(retry, circuitBreaker);
exports.handleRequest = async (req, res) => {
// Call your database safely!
const data = await retryWithBreaker.execute(() => database.getInfo(req.params.id));
return res.json(data);
};
I recommend reading the Polly wiki for more information for details and mechanics around the patterns we provide.
IPolicy (the shape of a policy)PolicyPolicy.handleAll()Policy.handleType(ctor[, filter])policy.orType(ctor[, filter])Policy.handleWhen(filter)policy.orWhen(filter)Policy.handleResultType(ctor[, filter])policy.orResultType(ctor[, filter])Policy.handleResultWhen(filter)policy.orWhenResult(filter)Policy.wrap(...policies)Policy.use(policy)Policy.noopCancellationTokennew CancellationTokenSource([parent])cancellationTokenSource.tokencancellationTokenSource.cancel()cancellationToken.isCancellationRequestedcancellationToken.onCancellationRequested(callback)cancellationToken.cancelled([cancellationToken])Event.toPromise(event[, cancellationToken])Event.once(event, callback)Policy.retry()retry.execute(fn[, cancellationToken])retry.attempts(count)retry.delay(amount)retry.exponential(options)retry.delegate(fn)retry.backoff(policy)retry.dangerouslyUnref()retry.onRetry(callback)retry.onSuccess(callback)retry.onFailure(callback)retry.onGiveUp(callback)Policy.circuitBreaker(openAfter, breaker)ConsecutiveBreakerSamplingBreakerbreaker.execute(fn[, cancellationToken])breaker.statebreaker.onBreak(callback)breaker.onReset(callback)breaker.onHalfOpen(callback)breaker.onStateChange(callback)breaker.onSuccess(callback)breaker.onFailure(callback)breaker.isolate()Policy.timeout(duration, strategy)timeout.dangerouslyUnref()timeout.execute(fn[, cancellationToken])timeout.onTimeout(callback)timeout.onSuccess(callback)timeout.onFailure(callback)Policy.bulkhead(limit[, queue])bulkhead.execute(fn[, cancellationToken])bulkhead.onReject(callback)bulkhead.onSuccess(callback)bulkhead.onFailure(callback)bulkhead.executionSlotsbulkhead.queueSlotsPolicy.fallback(valueOrFactory)fallback.execute(fn[, cancellationToken])fallback.onSuccess(callback)fallback.onFailure(callback)IPolicy (the shape of a policy)All Cockatiel fault handling policies (fallbacks, circuit breakers, bulkheads, timeouts, retries) adhere to the same interface. In TypeScript, this is given as:
export interface IPolicy<ContextType extends { cancellationToken: CancellationToken }> {
/**
* Fires on the policy when a request successfully completes and some
* successful value will be returned. In a retry policy, this is fired once
* even if the request took multiple retries to succeed.
*/
readonly onSuccess: Event<ISuccessEvent>;
/**
* Fires on the policy when a request fails *due to a handled reason* fails
* and will give rejection to the called.
*/
readonly onFailure: Event<IFailureEvent>;
/**
* Runs the function through behavior specified by the policy.
*/
execute<T>(
fn: (context: ContextType) => PromiseLike<T> | T,
cancellationToken?: CancellationToken,
): Promise<T>;
}
If you don't read TypeScript often, here's what it means:
onSuccess/onFailure, that are called when a call succeeds or fails. Note that onFailure only is called if a handled error is thrown.As a design decision, Cockatiel won't assume all thrown errors are actually failures unless you tell us. For example, in your application you might have errors thrown if the user submits invalid input, and triggering fault handling behavior for this reason would not be desirable!
execute function that you can use to "wrap" your own function. Anything you return from that function is returned, in a promise, from execute. You can optionally pass a cancellation token to the execute() function, and the function will always be called with an object at least containing a cancellation token (some policies might add extra metadata for you).PolicyThe Policy defines how errors and results are handled. Everything in Cockatiel ultimately deals with handling errors or bad results. The Policy sets up how
Policy.handleAll()Tells the policy to handle all errors.
Policy.handleAll();
// ...
Policy.handleType(ctor[, filter])policy.orType(ctor[, filter])Tells the policy to handle errors of the given type, passing in the contructor. If a filter function is also passed, we'll only handle errors if that also returns true.
Policy.handleType(NetworkError).orType(HttpError, err => err.statusCode === 503);
// ...
Policy.handleWhen(filter)policy.orWhen(filter)Tells the policy to handle any error for which the filter returns truthy
Policy.handleWhen(err => err instanceof NetworkError).orWhen(err => err.shouldRetry === true);
// ...
Policy.handleResultType(ctor[, filter])policy.orResultType(ctor[, filter])Tells the policy to treat certain return values of the function as errors--retrying if they appear, for instance. Results will be retried if they're an instance of the given class. If a filter function is also passed, we'll only treat return values as errors if that also returns true.
Policy.handleResultType(ReturnedNetworkError).orResultType(
HttpResult,
res => res.statusCode === 503,
);
// ...
Policy.handleResultWhen(filter)policy.orWhenResult(filter)Tells the policy to treat certain return values of the function as errors--retrying if they appear, for instance. Results will be retried the filter function returns true.
Policy.handleResultWhen(res => res.statusCode === 503).orWhenResult(res => res.statusCode === 429);
// ...
Policy.wrap(...policies)Wraps the given set of policies into a single policy. For instance, this:
const result = await retry.execute(() =>
breaker.execute(() => timeout.execute(({ cancellationToken }) => getData(cancellationToken))),
);
Is the equivalent to:
const result = await Policy
.wrap(retry, breaker, timeout)
.execute(({ cancellationToken }) => getData(cancellationToken)));
The context argument passed to the executed function is the merged object of all previous policies. So for instance, in the above example you'll get the cancellation token from the TimeoutPolicy as well as the attempt number from the RetryPolicy:
Policy.wrap(retry, breaker, timeout).execute(context => {
console.log(context);
// => { attempts: 1, cancellation: }
});
Policy.use(policy)A decorator that can be used to wrap class methods and apply the given policy to them. It also adds the last argument normally given in Policy.execute as the last argument in the function call. For example:
import { Policy } from 'cockatiel';
const retry = Policy.handleAll().retry().attempts(3);
class Database {
@Policy.use(retry)
public getUserInfo(userId, context) {
console.log('Retry attempt number', context.attempt);
// implementation here
}
}
const db = new Database();
db.getUserInfo(3).then(info => console.log('User 3 info:', info));
Note that it will force the return type to be a Promise, since that's what policies return.
Policy.noopA no-op policy, which may be useful for tests and stubs.
import { Policy } from 'cockatiel';
const policy = isProduction ? Policy.handleAll().retry().attempts(3) : Policy.noop;
export async function handleRequest() {
return policy.execute(() => getInfoFromDatabase());
}
Backoff algorithms are immutable. The backoff class adheres to the interface:
export interface IBackoffFactory<T> {
/**
* Returns the next backoff duration. Can return "undefined" to signal
* that we should stop backing off.
*/
next(context: T): IBackoff<T> | undefined;
}
The backoff, returned from the next() call, has the appropriate delay and next() method again.
export interface IBackoff<T> {
next(context: T): IBackoff<T> | undefined; // same as above
/**
* Returns the number of milliseconds to wait for this backoff attempt.
*/
readonly duration: number;
}
A backoff that backs off for a constant amount of time, and can optionally stop after a certain number of attempts.
// Waits 50ms between back offs, forever
const foreverBackoff = new ConstantBackoff(50);
// Waits 50ms and stops backing off after three attempts.
const limitedBackoff = new ConstantBackoff(50, 3);
Tip: exponential backoffs and circuit breakers are great friends!
The crowd favorite. By default, it uses a decorrelated jitter algorithm, which is a good default for most applications. Takes in an options object, which can have any of these properties:
export interface IExponentialBackoffOptions<S> {
/**
* Delay generator function to use. This package provides several of these/
* Defaults to "decorrelatedJitterGenerator", a good default for most
* scenarios (see the linked Polly issue).
*
* @see https://github.com/App-vNext/Polly/issues/530
* @see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
*/
generator: GeneratorFn<S>;
/**
* Maximum delay, in milliseconds. Defaults to 30s.
*/
maxDelay: number;
/**
* Maximum retry attempts. Defaults to Infinity.
*/
maxAttempts: number;
/**
* Backoff exponent. Defaults to 2.
*/
exponent: number;
/**
* The initial, first delay of the backoff, in milliseconds.
* Defaults to 128ms.
*/
initialDelay: number;
}
Example:
```ts import { ExponentialBack
$ claude mcp add cockatiel \
-- python -m otcore.mcp_server <graph>