[opossum] v4.0 updates. (#38017)

The lib now exposes a constructor directly, so the export has been
changed to use the `export =` with `declare` style instead of `export default`.

- `promisify` removed.
- References to `hystrix` were removed.
- The primary class now has generics the pulls the argument and return
  types from the constructor then passes them to the result of `fire`
  and some of the event listeners.
- Event listeners were added to give users types in the specific callbacks.
- Doc updates for the methods.
- The `action` function must now return a Promise.
This commit is contained in:
Matt R. Wilson 2019-09-09 22:29:58 -06:00 committed by Mine Starks
parent 970c733cdd
commit 5db94ca7fd
2 changed files with 268 additions and 221 deletions

View File

@ -1,20 +1,17 @@
// Type definitions for opossum 1.10
// Type definitions for opossum 4.0
// Project: https://github.com/nodeshift/opossum, https://nodeshift.dev/opossum
// Definitions by: Quinn Langille <https://github.com/quinnlangille>
// Willy Zhang <https://github.com/merufm>
// Lance Ball <https://github.com/lance>
// Matt R. Wilson <https://github.com/mastermatt>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0
/// <reference types="node"/>
import * as stream from "stream";
import { EventEmitter } from "events";
export type Action = (...args: any[]) => any;
export class CircuitBreaker extends EventEmitter {
constructor(action: Action, options: CircuitBreakerOptions);
declare class CircuitBreaker<TI extends unknown[] = unknown[], TR = unknown> extends EventEmitter {
constructor(action: (...args: TI) => Promise<TR>, options?: CircuitBreaker.Options);
readonly name: string;
readonly group: string;
@ -24,182 +21,228 @@ export class CircuitBreaker extends EventEmitter {
readonly opened: boolean;
readonly halfOpen: boolean;
readonly isShutdown: boolean;
readonly status: Status;
readonly stats: Stats;
readonly hystrixStats: HystrixStats;
readonly status: CircuitBreaker.Status;
readonly stats: CircuitBreaker.Stats;
readonly warmUp: boolean;
readonly volumeThreshold: number;
/**
* Clears the cache of this CircuitBreaker
*/
clearCache(): void;
open(): void;
/**
* Closes the breaker, allowing the action to execute again
*/
close(): void;
disable(): void;
enable(): void;
fallback(func: Action | CircuitBreaker): this;
fire(...args: any[]): Promise<any>;
healthCheck(
func: (...args: any[]) => Promise<any>,
interval?: number
): void;
/**
* Opens the breaker.
* Each time the breaker is fired while the circuit is opened, a failed Promise is
* returned, or if any fallback function has been provided, it is invoked.
*/
open(): void;
/**
* Shuts down this circuit breaker.
* All subsequent calls to the circuit will fail, returning a rejected promise.
*/
shutdown(): void;
/**
* Disables this circuit, causing all calls to the circuit's function to be
* executed without circuit or fallback protection.
*/
disable(): void;
/**
* Enables this circuit. If the circuit is the disabled state, it will be re-enabled.
* If not, this is essentially a noop.
*/
enable(): void;
/**
* Provide a fallback function for this CircuitBreaker.
* This function will be executed when the circuit is fired and fails.
* It will always be preceded by a `failure` event, and `breaker.fire` returns a rejected Promise.
*/
fallback(func: ((...args: any[]) => any) | CircuitBreaker): this;
/**
* Execute the action for this circuit.
* If the action fails or times out, the returned promise will be rejected.
* If the action succeeds, the promise will resolve with the resolved value from action.
* If a fallback function was provided, it will be invoked in the event of any failure or timeout.
*/
fire(...args: TI): Promise<TR>;
/**
* Provide a health check function to be called periodically.
* The function should return a Promise. If the promise is rejected the circuit will open.
* This is in addition to the existing circuit behavior as defined by the
* `errorThresholdPercentage` option passed to the constructor.
* For example, if the health check function provided here always returns a resolved promise,
* the circuit can still trip and open if there are failures exceeding the configured threshold.
* The health check function is executed within the circuit breaker's execution context,
* so this within the function is the circuit breaker itself.
*
* The interval is the amount of time between calls to the health check function.
* Default: 5000 (5 seconds)
*/
healthCheck(func: () => Promise<void>, interval?: number): void;
/* tslint:disable:unified-signatures */
on(event: "halfOpen", listener: (resetTimeout: number) => void): this;
on(event: "close", listener: () => void): this;
on(event: "open", listener: () => void): this;
on(event: "shutdown", listener: () => void): this;
on(event: "fire", listener: (args: TI) => void): this;
on(event: "cacheHit", listener: () => void): this;
on(event: "cacheMiss", listener: () => void): this;
on(event: "reject", listener: (err: Error) => void): this;
on(event: "timeout", listener: (err: Error) => void): this;
on(event: "success", listener: (result: TR, latencyMs: number) => void): this;
on(event: "semaphoreLocked", listener: (err: Error) => void): this;
on(event: "healthCheckFailed", listener: (err: Error) => void): this;
on(event: "fallback", listener: (result: unknown, err: Error) => void): this;
on(event: "failure", listener: (err: Error, latencyMs: number, args: TI) => void): this;
/* tslint:enable:unified-signatures */
}
export enum Event {
cacheHit = "cacheHit",
cacheMiss = "cacheMiss",
close = "close",
failure = "failure",
fallback = "fallback",
fire = "fire",
halfOpen = "halfOpen",
healthCheckFailed = "health-check-failed",
open = "open",
reject = "reject",
semaphoreLocked = "semaphore-locked",
success = "success",
timeout = "timeout"
declare namespace CircuitBreaker {
interface Options {
/**
* The time in milliseconds that action should be allowed to execute before timing out.
* @default 10000 (10 seconds)
*/
timeout?: number;
/**
* The number of times the circuit can fail before opening.
* @default 10
* @deprecated see options.errorThresholdPercentage
*/
maxFailures?: number;
/**
* The time in milliseconds to wait before setting the breaker to `halfOpen` state, and trying the action again.
* @default 30000 (30 seconds)
*/
resetTimeout?: number;
/**
* Sets the duration of the statistical rolling window, in milliseconds.
* This is how long Opossum keeps metrics for the circuit breaker to use and for publishing.
* @default 10000
*/
rollingCountTimeout?: number;
/**
* Sets the number of buckets the rolling statistical window is divided into.
* So, if options.rollingCountTimeout is 10,000, and options.rollingCountBuckets is 10, then the
* statistical window will be 1,000 1 second snapshots in the statistical window.
* @default 10
*/
rollingCountBuckets?: number;
/**
* The circuit name to use when reporting stats.
* Defaults to the name of the function this circuit controls then falls back to a UUID
*/
name?: string;
/**
* A grouping key for reporting.
* Defaults to the computed value of `name`
*/
group?: string;
/**
* This property indicates whether execution latencies should be tracked and calculated as percentiles.
* If they are disabled, all summary statistics (mean, percentiles) are returned as -1.
* @default false
*/
rollingPercentilesEnabled?: boolean;
/**
* The number of concurrent requests allowed.
* If the number currently executing function calls is equal to options.capacity, further calls
* to `fire()` are rejected until at least one of the current requests completes.
* @default MAX_SAFE_INTEGER
*/
capacity?: number;
/**
* The error percentage at which to open the circuit and start short-circuiting requests to fallback.
* @default 50
*/
errorThresholdPercentage?: number;
/**
* Whether this circuit is enabled upon construction.
* @default true
*/
enabled?: boolean;
/**
* Determines whether to allow failures without opening the circuit during a brief warmup period (`rollingCountDuration`)
* This can help in situations where no matter what your `errorThresholdPercentage` is, if the
* first execution times out or fails, the circuit immediately opens.
* @default false
*/
allowWarmUp?: boolean;
/**
* The minimum number of requests within the rolling statistical window that must exist before
* the circuit breaker can open. This is similar to `allowWarmUp` in that no matter how many
* failures there are, if the number of requests within the statistical window does not exceed
* this threshold, the circuit will remain closed.
* @default 0
*/
volumeThreshold?: number;
/**
* An optional function that will be called when the circuit's function fails (returns a rejected Promise).
* If this function returns truthy, the circuit's `failPure` statistics will not be incremented.
* This is useful, for example, when you don't want HTTP 404 to trip the circuit, but still want to handle it as a failure case.
*/
errorFilter?: () => boolean;
/**
* Whether the return value of the first successful execution of the circuit's function will be cached.
* Once a value has been cached that value will be returned for every subsequent execution: the cache can be cleared using `clearCache`.
* (The metrics cacheHit and cacheMiss reflect cache activity.)
* @default false
*/
cache?: boolean;
}
interface Status extends EventEmitter {
stats: Stats;
window: Window;
on(event: "snapshot", listener: (snapshot: Stats) => void): this;
}
interface Bucket {
failures: number;
fallbacks: number;
successes: number;
rejects: number;
fires: number;
timeouts: number;
cacheHits: number;
cacheMisses: number;
semaphoreRejections: number;
percentiles: { [percentile: number]: number };
latencyTimes: number[];
}
type Window = Bucket[];
interface Stats extends Bucket {
latencyMean: number;
}
}
export interface CircuitBreakerOptions {
/**
* The time in milliseconds that action should be allowed to execute before timing out.
* Timeout can be disabled by setting this to `false`.
*/
timeout?: number | false;
/**
* The number of times the circuit can fail before opening.
* @deprecated see options.errorThresholdPercentage
*/
maxFailures?: number;
/**
* The time in milliseconds to wait before setting the breaker to `halfOpen` state, and trying the action again.
*/
resetTimeout?: number;
/**
* Sets the duration of the statistical rolling window, in milliseconds.
* This is how long Opossum keeps metrics for the circuit breaker to use and for publishing.
* @default 10000
*/
rollingCountTimeout?: number;
/**
* Sets the number of buckets the rolling statistical window is divided into.
* So, if options.rollingCountTimeout is 10,000, and options.rollingCountBuckets is 10, then the
* statistical window will be 1,000 1 second snapshots in the statistical window.
* @default 10
*/
rollingCountBuckets?: number;
/**
* The circuit name to use when reporting stats.
* Defaults to the name of the action function then falls back to a UUID
*/
name?: string;
/**
* A grouping key for reporting.
* Defaults to the computed value of options.name
*/
group?: string;
/**
* This property indicates whether execution latencies should be tracked and calculated as percentiles.
* If they are disabled, all summary statistics (mean, percentiles) are returned as -1.
* @default true
*/
rollingPercentilesEnabled?: boolean;
/**
* The number of concurrent requests allowed.
* If the number currently executing function calls is equal to options.capacity, further calls
* to `fire()` are rejected until at least one of the current requests completes.
* @default MAX_SAFE_INTEGER
*/
capacity?: number;
/**
* The error percentage at which to open the circuit and start short-circuiting requests to fallback.
*/
errorThresholdPercentage?: number;
/**
* Whether this circuit is enabled upon construction.
* @default true
*/
enabled?: boolean;
/**
* Determines whether to allow failures without opening the circuit during a brief warmup period
* This can help in situations where no matter what your `errorThresholdPercentage` is, if the
* first execution times out or fails, the circuit immediately opens.
* @default false
*/
allowWarmUp?: boolean;
/**
* The minimum number of requests within the rolling statistical window that must exist before
* the circuit breaker can open. This is similar to `allowWarmUp` in that no matter how many
* failures there are, if the number of requests within the statistical window does not exceed
* this threshold, the circuit will remain closed.
* @default 0
*/
volumeThreshold?: number;
/**
* If set to true, the value from the first call to `fire` will be cached an subsequent calls
* will not execute the `action` function, but return the cached value instead.
* @default false
*/
cache?: boolean;
}
export interface Status extends EventEmitter {
stats: Stats;
window: Window;
increment(property: string, latencyRunTime?: number): void;
open(): void;
close(): void;
}
export interface Bucket {
failures: number;
fallbacks: number;
successes: number;
rejects: number;
fires: number;
timeouts: number;
cacheHits: number;
cacheMisses: number;
semaphoreRejections: number;
percentiles: { [percentile: number]: number };
latencyTimes: number[];
}
export type Window = Bucket[];
export interface Stats extends Bucket {
latencyMean: number;
}
export class HystrixStats {
constructor(circuit: CircuitBreaker);
getHystrixStream(): stream.Transform;
}
export function promisify(action: Action): (...args: any[]) => Promise<any>;
export const stats: stream.Transform;
interface index {
(action: Action, options: CircuitBreakerOptions): CircuitBreaker;
promisify: (action: Action) => (...args: any[]) => Promise<any>;
stats: stream.Transform;
}
export const circuitBreaker: index;
export default circuitBreaker;
export = CircuitBreaker;

View File

@ -1,34 +1,25 @@
import * as fs from "fs";
import circuitBreaker, {
CircuitBreaker,
CircuitBreakerOptions,
promisify,
stats,
Stats,
Window
} from "opossum";
let readFile = promisify(fs.readFile);
stats.removeAllListeners();
import * as CircuitBreaker from "opossum";
import { promisify } from "util";
let breaker: CircuitBreaker;
const callbackNoArgs = () => console.log("foo");
const callbackNoArgs = async () => console.log("foo");
breaker = circuitBreaker(() => true, {
timeout: false,
maxFailures: 50,
resetTimeout: 10,
rollingCountTimeout: 500,
rollingCountBuckets: 20,
name: "test",
group: "group",
rollingPercentilesEnabled: true,
capacity: 1,
errorThresholdPercentage: 1,
enabled: true,
allowWarmUp: true,
volumeThreshold: 1,
cache: true
breaker = new CircuitBreaker(async () => true, {
timeout: 1000,
maxFailures: 50,
resetTimeout: 10,
rollingCountTimeout: 500,
rollingCountBuckets: 20,
name: "test",
group: "group",
rollingPercentilesEnabled: true,
capacity: 1,
errorThresholdPercentage: 1,
enabled: true,
allowWarmUp: true,
volumeThreshold: 1,
cache: true
});
breaker.name; // $ExpectType string
@ -43,7 +34,6 @@ breaker.isShutdown; // $ExpectType boolean
breaker.volumeThreshold; // $ExpectType number
breaker.status.stats.latencyMean; // $ExpectType number
breaker.stats.latencyTimes; // $ExpectType number[]
breaker.hystrixStats.getHystrixStream().removeAllListeners();
breaker.clearCache(); // $ExpectType void
breaker.open(); // $ExpectType void
@ -52,6 +42,22 @@ breaker.disable(); // $ExpectType void
breaker.enable(); // $ExpectType void
breaker.shutdown(); // $ExpectType void
// Check the generic types pass down correctly from constructor to `fire` and events.
const action = async (foo: string, bar: number) => {
return foo ? bar : bar * 2;
};
const typedBreaker = new CircuitBreaker(action);
typedBreaker.fire(5, "hello"); // $ExpectError
typedBreaker.fire("hello world", 42); // $ExpectType Promise<number>
typedBreaker.on("success", (result, latencyMs) => {
result; // $ExpectType number
latencyMs; // $ExpectType number
});
typedBreaker.on("fire", ([foo, bar]) => {
foo; // $ExpectType string
bar; // $ExpectType number
});
// The following are examples are from the libs README and official documentation
// https://nodeshift.github.io/opossum/index.html.
@ -62,25 +68,25 @@ function asyncFunctionThatCouldFail(x: any, y: any) {
});
}
const options: CircuitBreakerOptions = {
timeout: 3000, // If our function takes longer than 3 seconds, trigger a failure
errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit
resetTimeout: 30000 // After 30 seconds, try again.
const options: CircuitBreaker.Options = {
timeout: 3000, // If our function takes longer than 3 seconds, trigger a failure
errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit
resetTimeout: 30000 // After 30 seconds, try again.
};
breaker = circuitBreaker(asyncFunctionThatCouldFail, options);
breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);
breaker
.fire("foo")
.then(console.log)
.catch(console.error);
breaker = circuitBreaker(asyncFunctionThatCouldFail, options);
breaker = new CircuitBreaker(asyncFunctionThatCouldFail, options);
// if asyncFunctionThatCouldFail starts to fail, firing the breaker
// will trigger our fallback function
breaker.fallback(() => "Sorry, out of service right now");
breaker.on("fallback", result => console.log(result));
breaker = circuitBreaker(callbackNoArgs, options);
breaker = new CircuitBreaker(callbackNoArgs, options);
breaker.fallback(callbackNoArgs);
@ -92,28 +98,26 @@ breaker.on("halfOpen", callbackNoArgs);
breaker.on("close", callbackNoArgs);
breaker.on("fallback", data => console.log(data));
readFile = circuitBreaker.promisify(fs.readFile);
breaker = circuitBreaker(readFile, options);
const readFile = promisify(fs.readFile);
breaker = new CircuitBreaker(readFile, options);
breaker
.fire("./package.json", "utf-8")
.then(console.log)
.catch(console.error);
breaker = circuitBreaker(fs.readFile, {});
breaker.hystrixStats.getHystrixStream().pipe(process.stdout);
breaker = new CircuitBreaker(readFile, {});
// Creates a 1 second window consisting of ten time slices,
// each 100ms long.
const circuit = circuitBreaker(fs.readFile, {
rollingCountBuckets: 10,
rollingCountTimeout: 1000
const circuit = new CircuitBreaker(readFile, {
rollingCountBuckets: 10,
rollingCountTimeout: 1000
});
// get the cumulative statistics for the last second
const theStats: Stats = breaker.status.stats;
const theStats: CircuitBreaker.Stats = breaker.status.stats;
// get the array of 10, 1 second time slices for the last second
const window: Window = breaker.status.window;
const window: CircuitBreaker.Window = breaker.status.window;
window[0].fires; // $ExpectType number