node-circuit-breaker
Version:
Circuit Breaker: Decorators and tools that can easily apply the Circuit Breaker pattern.
206 lines (205 loc) • 9.56 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreakerManager = exports.Circuit = void 0;
const path = __importStar(require("path"));
const execution_timeout_error_1 = require("./exceptions/execution-timeout-error");
const axios_extended_error_1 = require("./exceptions/axios-extended-error");
const typeorm_extended_error_1 = require("./exceptions/typeorm-extended-error");
const circuit_type_1 = require("./types/circuit.type");
const worker = require('./circuit-breaker-worker');
let seq = 0;
class Circuit {
constructor(caller, funcName, options) {
this.state = circuit_type_1.CircuitBreakerState.Closed;
this._serializedOptions = null;
this.id = (++seq).toString();
this.caller = caller + ':' + funcName;
this.options = options;
}
get serializedOptions() {
if (!this._serializedOptions)
this._serializedOptions = {
fallbackForSeconds: this.options.fallbackForSeconds,
rules: this.options.rules.map((item) => ({
exceptions: item.exceptions.map((errorType) => errorType.name),
times: item.times,
inSeconds: item.inSeconds,
})),
};
return this._serializedOptions;
}
setState(state) {
return __awaiter(this, void 0, void 0, function* () {
// console.log(this, state);
if (state === circuit_type_1.CircuitBreakerState.Open && this.options.onCircuitOpen) {
if (!(yield this.options.onCircuitOpen(this)))
return false;
}
else if (state === circuit_type_1.CircuitBreakerState.Closed && this.options.onCircuitClose) {
if (!(yield this.options.onCircuitClose(this)))
return false;
}
this.state = state;
return true;
});
}
invoke(context, func, trace, args) {
if (this.state === circuit_type_1.CircuitBreakerState.Open) {
if (typeof this.options.fallback === 'function')
return this.options.fallback.apply(context, args);
else if (typeof this.options.fallback === 'string' &&
this.options.fallback in context &&
typeof context[this.options.fallback] === 'function') {
return context[this.options.fallback].apply(context, args);
}
else
return this.options.fallback;
}
try {
let result;
if (this.options.timeoutMilliSeconds) {
const begin = Date.now();
result = func.apply(context, args);
if (result instanceof Promise) {
return result.then((result) => {
this.pushTimeoutError(trace, Date.now() - begin, args);
return result;
});
}
else {
this.pushTimeoutError(trace, Date.now() - begin, args);
return result;
}
}
else
result = func.apply(context, args);
if (this.state === circuit_type_1.CircuitBreakerState.HalfOpen) {
this.setState(circuit_type_1.CircuitBreakerState.Closed).then((success) => {
if (success)
worker.setState(this.id, circuit_type_1.CircuitBreakerState.Closed);
});
}
return result;
}
catch (error) {
// console.log('invoke error', error);
this.pushError(trace, error, args);
throw error;
}
}
pushTimeoutError(trace, timeTaken, args) {
if (this.options.timeoutMilliSeconds < timeTaken) {
const message = `CircuitBreaker: ${this.caller} execution timeout ${timeTaken} > ${this.options.timeoutMilliSeconds}`;
this.pushError(trace, new execution_timeout_error_1.ExecutionTimeoutError(message, timeTaken), args);
}
}
pushError(trace, error, args) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function* () {
error = Circuit.exceptionPipes.reduce((result, item) => item(result), error);
// console.log(error);
// console.log((trace as any).stack);
if (yield ((_b = (_a = this.options).ignore) === null || _b === void 0 ? void 0 : _b.call(_a, error, this)))
return;
// this.records.push({ error, time: new Date(), args, stack: trace?.stack });
yield ((_d = (_c = this.options).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error, this));
worker.pushError(this.id, error.constructor.name);
if (this.state === circuit_type_1.CircuitBreakerState.HalfOpen) {
this.state = circuit_type_1.CircuitBreakerState.Open;
worker.setState(this.id, circuit_type_1.CircuitBreakerState.Open);
}
});
}
}
exports.Circuit = Circuit;
Circuit.exceptionPipes = [axios_extended_error_1.AxiosExceptionPipe, typeorm_extended_error_1.TypeormExceptionPipe];
function _getCallerFile() {
const err = new Error();
Error.prepareStackTrace = (_, stack) => stack;
const stack = err.stack;
Error.prepareStackTrace = undefined;
const paths = stack
.filter((callSite) => {
const file = callSite.getFileName() || '';
return file && !file.match(/reflect-metadata/i) && !(callSite.getFunctionName() || '').toString().match(/__decorate/);
})
.map((callSite) => [path.relative(process.cwd(), callSite.getFileName() || ''), callSite.getLineNumber() || ''].join(':'));
const idx = paths.findIndex((path) => path.match(/circuit-breaker.decorator/));
if (idx < 0)
throw new Error('CircuitBreaker: Call stack is ambiguous.');
return paths[idx + 1];
}
class CircuitBreakerManager {
static invoke(context, func, funcName, options, trace, ...args) {
var _a;
let circuit;
if (options.scope === circuit_type_1.CircuitBreakerScope.INSTANCE) {
if (!context.__circuit)
Reflect.defineProperty(context, '__circuit', { value: [], writable: false, configurable: false, enumerable: false });
circuit = (_a = context.__circuit.find((item) => item.func === func)) === null || _a === void 0 ? void 0 : _a.circuit;
if (!circuit) {
circuit = new Circuit(_getCallerFile(), funcName, options);
context.__circuit.push({ func, circuit });
CircuitBreakerManager.circuits[circuit.id] = circuit;
}
}
else {
circuit = func.__circuit;
if (!func.__circuit) {
circuit = new Circuit(_getCallerFile(), funcName, options);
Reflect.defineProperty(func, '__circuit', { value: circuit, writable: false, configurable: false, enumerable: false });
CircuitBreakerManager.circuits[circuit.id] = circuit;
}
}
return circuit.invoke(context, func, trace, args);
}
static terminate() {
return __awaiter(this, void 0, void 0, function* () {
return worker.terminate();
});
}
}
exports.CircuitBreakerManager = CircuitBreakerManager;
CircuitBreakerManager.circuits = {};
worker.setMetadataProvider((id) => {
var _a;
return (_a = CircuitBreakerManager.circuits[id]) === null || _a === void 0 ? void 0 : _a.serializedOptions;
});
worker.setStateCallback((id, state) => __awaiter(void 0, void 0, void 0, function* () {
const circuit = CircuitBreakerManager.circuits[id];
if (!circuit)
return true;
return circuit.setState(state);
}));