diff --git a/types/opossum/index.d.ts b/types/opossum/index.d.ts index e79a9c4bba..b17a1dd0be 100644 --- a/types/opossum/index.d.ts +++ b/types/opossum/index.d.ts @@ -3,6 +3,7 @@ // Definitions by: Quinn Langille // Willy Zhang // Lance Ball +// Matt R. Wilson // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 @@ -13,63 +14,190 @@ import { EventEmitter } from "events"; export type Action = (...args: any[]) => any; export class CircuitBreaker extends EventEmitter { + constructor(action: Action, options: CircuitBreakerOptions); + + readonly name: string; + readonly group: string; + readonly enabled: boolean; + readonly pendingClose: boolean; + readonly closed: boolean; + readonly opened: boolean; + readonly halfOpen: boolean; + readonly status: Status; + readonly stats: Stats; + readonly hystrixStats: HystrixStats; + readonly warmUp: boolean; + readonly volumeThreshold: number; + clearCache(): void; + open(): void; close(): void; disable(): void; enable(): void; - fallback(func?: (...args: any[]) => any): this; + fallback(func: Action | CircuitBreaker): this; fire(...args: any[]): Promise; - healthCheck(func: (...args: any[]) => Promise, interval?: number): Promise; - on(event: string | symbol, listener: (...args: any[]) => void): this; - open(): void; - promisify(action: Action): Promise; - stats(): stream.Transform; - - static readonly name: symbol; - static readonly group: symbol; - static readonly pendingClose: symbol; - static readonly closed: symbol; - static readonly opened: symbol; - static readonly halfOpen: symbol; - static readonly status: symbol; - static readonly hystrixStats: symbol; - static readonly enabled: symbol; - static readonly warmUp: symbol; - static readonly volumeThreshold: symbol; + healthCheck( + func: (...args: any[]) => Promise, + interval?: number + ): void; } 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' + 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" } export interface CircuitBreakerOptions { - timeout?: number; + /** + * 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 default function circuitBreaker( - action: Action, - options?: CircuitBreakerOptions -): CircuitBreaker; +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; diff --git a/types/opossum/opossum-tests.ts b/types/opossum/opossum-tests.ts index 9ec5425416..ebbf0bea47 100644 --- a/types/opossum/opossum-tests.ts +++ b/types/opossum/opossum-tests.ts @@ -1,48 +1,117 @@ -import { Transform } from "stream"; -import circuitBreaker, { CircuitBreaker, CircuitBreakerOptions } from "opossum"; +import * as fs from "fs"; +import circuitBreaker, { + CircuitBreaker, + CircuitBreakerOptions, + promisify, + stats, + Stats, + Window +} from "opossum"; -const _blank = () => {}; -const async_blank = () => new Promise(resolve => null); +let readFile = promisify(fs.readFile); +stats.removeAllListeners(); -const testNoOptions: CircuitBreaker = circuitBreaker(_blank); +let breaker: CircuitBreaker; +const callbackNoArgs = () => console.log("foo"); -const options: CircuitBreakerOptions = { - timeout: 1, - maxFailures: 1, - resetTimeout: 1, - rollingCountTimeout: 1, - rollingCountBuckets: 1, - name: "testing", +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 + volumeThreshold: 1, + cache: true +}); + +breaker.name; // $ExpectType string +breaker.group; // $ExpectType string +breaker.enabled; // $ExpectType boolean +breaker.pendingClose; // $ExpectType boolean +breaker.closed; // $ExpectType boolean +breaker.opened; // $ExpectType boolean +breaker.halfOpen; // $ExpectType boolean +breaker.warmUp; // $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 +breaker.close(); // $ExpectType void +breaker.disable(); // $ExpectType void +breaker.enable(); // $ExpectType void + +// The following are examples are from the libs README and official documentation +// https://nodeshift.github.io/opossum/index.html. + +function asyncFunctionThatCouldFail(x: any, y: any) { + return new Promise((resolve, reject) => { + // Do something, maybe on the network or a disk + resolve([x, y]); + }); +} + +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. }; +breaker = circuitBreaker(asyncFunctionThatCouldFail, options); -const testWithOptions: CircuitBreaker = circuitBreaker(_blank, options); -const shouldBeAPromise: Promise = testWithOptions.promisify(_blank); -const shouldBeATransformStream: Transform = testWithOptions.stats(); +breaker + .fire("foo") + .then(console.log) + .catch(console.error); -const healthCheck: Promise = testWithOptions.healthCheck(async_blank, 8); -const shouldBeACircuitBreaker: CircuitBreaker = testWithOptions.fallback(); -const shouldBeAPromiseGeneric: Promise = testWithOptions.fire(); +breaker = 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)); -const shouldBeVoid = (): void => testWithOptions.enable(); -const shouldBeVoid2 = (): void => testWithOptions.disable(); -const shouldBeVoid3 = (): void => testWithOptions.close(); -const shouldBeVoid4 = (): void => testWithOptions.open(); -const shouldBeVoid5 = (): void => testWithOptions.clearCache(); +breaker = circuitBreaker(callbackNoArgs, options); -const shouldBeSymbol: symbol = CircuitBreaker.name; -const shouldBeSymbol2: symbol = CircuitBreaker.group; -const shouldBeSymbol3: symbol = CircuitBreaker.pendingClose; -const shouldBeSymbol4: symbol = CircuitBreaker.closed; -const shouldBeSymbol5: symbol = CircuitBreaker.opened; -const shouldBeSymbol6: symbol = CircuitBreaker.halfOpen; -const shouldBeSymbol7: symbol = CircuitBreaker.hystrixStats; -const shouldBeSymbol8: symbol = CircuitBreaker.status; -const shouldBeSymbol9: symbol = CircuitBreaker.enabled; -const shouldBeSymbol10: symbol = CircuitBreaker.warmUp; -const shouldBeSymbol11: symbol = CircuitBreaker.volumeThreshold; +breaker.fallback(callbackNoArgs); + +breaker.on("success", result => console.log(result)); +breaker.on("timeout", callbackNoArgs); +breaker.on("reject", callbackNoArgs); +breaker.on("open", callbackNoArgs); +breaker.on("halfOpen", callbackNoArgs); +breaker.on("close", callbackNoArgs); +breaker.on("fallback", data => console.log(data)); + +readFile = circuitBreaker.promisify(fs.readFile); +breaker = 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); + +// Creates a 1 second window consisting of ten time slices, +// each 100ms long. +const circuit = circuitBreaker(fs.readFile, { + rollingCountBuckets: 10, + rollingCountTimeout: 1000 +}); + +// get the cumulative statistics for the last second +const theStats: Stats = breaker.status.stats; + +// get the array of 10, 1 second time slices for the last second +const window: Window = breaker.status.window; +window[0].fires; // $ExpectType number