From 5db94ca7fd3570dc23588b404d7bb669a7886a8a Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Mon, 9 Sep 2019 22:29:58 -0600 Subject: [PATCH] [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. --- types/opossum/index.d.ts | 395 ++++++++++++++++++--------------- types/opossum/opossum-tests.ts | 94 ++++---- 2 files changed, 268 insertions(+), 221 deletions(-) diff --git a/types/opossum/index.d.ts b/types/opossum/index.d.ts index 5ff0d04bf7..454a56e863 100644 --- a/types/opossum/index.d.ts +++ b/types/opossum/index.d.ts @@ -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 // Willy Zhang // Lance Ball // Matt R. Wilson // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// TypeScript Version: 3.0 /// -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 extends EventEmitter { + constructor(action: (...args: TI) => Promise, 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; - healthCheck( - func: (...args: any[]) => Promise, - 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; + + /** + * 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, 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; -export const stats: stream.Transform; - -interface index { - (action: Action, options: CircuitBreakerOptions): CircuitBreaker; - - promisify: (action: Action) => (...args: any[]) => Promise; - stats: stream.Transform; -} - -export const circuitBreaker: index; -export default circuitBreaker; +export = CircuitBreaker; diff --git a/types/opossum/opossum-tests.ts b/types/opossum/opossum-tests.ts index 69d64497f0..8a55844414 100644 --- a/types/opossum/opossum-tests.ts +++ b/types/opossum/opossum-tests.ts @@ -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 +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