node-circuit-breaker
Version:
Circuit Breaker: Decorators and tools that can easily apply the Circuit Breaker pattern.
168 lines (167 loc) • 7.02 kB
JavaScript
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 });
const worker_threads_1 = require("worker_threads");
var CircuitBreakerState;
(function (CircuitBreakerState) {
CircuitBreakerState[CircuitBreakerState["Closed"] = 0] = "Closed";
CircuitBreakerState[CircuitBreakerState["Open"] = 1] = "Open";
CircuitBreakerState[CircuitBreakerState["HalfOpen"] = 2] = "HalfOpen";
})(CircuitBreakerState || (CircuitBreakerState = {}));
const channel = new worker_threads_1.BroadcastChannel('worker');
if (worker_threads_1.isMainThread) {
let worker;
let terminated = false;
let metadataProvider;
let stateCallback;
function createWorker() {
if (terminated)
return;
worker = new worker_threads_1.Worker(__filename, { resourceLimits: {} });
worker.on('error', (e) => __awaiter(this, void 0, void 0, function* () {
console.error(e);
yield worker.terminate();
}));
worker.on('exit', (code) => {
if (!terminated && code !== 0)
console.error(`Worker stopped with exit code ${code}`);
createWorker();
});
}
createWorker();
channel.onmessage = ({ data: { mode, id, data } }) => __awaiter(void 0, void 0, void 0, function* () {
// console.log('parent got message', mode, id, data);
switch (mode) {
case 'metadata':
channel.postMessage({ mode, id, data: metadataProvider(id) });
break;
case 'circuit-open': {
if (!(yield stateCallback(id, CircuitBreakerState.Open)))
channel.postMessage({ mode: 'circuit-close', id });
break;
}
case 'circuit-halfopen': {
if (!(yield stateCallback(id, CircuitBreakerState.HalfOpen)))
channel.postMessage({ mode: 'circuit-open', id });
break;
}
case 'circuit-close': {
if (!(yield stateCallback(id, CircuitBreakerState.Closed)))
channel.postMessage({ mode: 'circuit-open', id });
break;
}
}
});
channel.onmessageerror = (ev) => {
console.error(ev);
};
module.exports = {
setMetadataProvider: (provider) => {
metadataProvider = provider;
},
setStateCallback: (callback) => {
stateCallback = callback;
},
setState: (id, state) => {
channel.postMessage({ mode: state === CircuitBreakerState.Open ? 'circuit-open' : 'circuit-close', id });
},
pushError: (id, errorType) => {
// console.log(id, errorType);
channel.postMessage({ mode: 'push-error', id, data: { errorType } });
},
terminate: () => __awaiter(void 0, void 0, void 0, function* () {
terminated = true;
yield (worker === null || worker === void 0 ? void 0 : worker.terminate());
channel.close();
}),
};
}
else {
const metadata = {};
const metadataRequested = {};
const errorRecords = {};
const openStates = {};
const debounce = {};
function openProcess(id) {
var _a;
delete debounce[id];
if (((_a = openStates[id]) === null || _a === void 0 ? void 0 : _a.state) === CircuitBreakerState.Open || !metadata[id]) {
// console.log('already open');
return;
}
const meta = metadata[id];
errorRecords[id] = errorRecords[id] || [];
const oldestTime = Date.now() - meta.rules.reduce((result, item) => Math.max(item.inSeconds), 0) * 1000;
while (errorRecords[id].length > 0 && errorRecords[id][0].time < oldestTime)
errorRecords[id].shift();
const records = errorRecords[id] || [];
if (meta.rules.some((rule) => {
const time = Date.now() - Math.max(rule.inSeconds) * 1000;
const filtered = records.filter((record) => rule.exceptions.includes(record.error) && record.time >= time);
return rule.times <= filtered.length;
})) {
openStates[id] = { state: CircuitBreakerState.Open, changed: Date.now() };
channel.postMessage({ mode: 'circuit-open', id });
}
}
setInterval(() => {
if (Object.keys(openStates).length <= 0)
return;
// console.log(openStates);
const now = Date.now();
const ids = Object.keys(openStates);
for (const id of ids) {
if (openStates[id].state !== CircuitBreakerState.Open)
continue;
const meta = metadata[id];
if (meta.fallbackForSeconds * 1000 + openStates[id].changed > now) {
openStates[id].state = CircuitBreakerState.HalfOpen;
channel.postMessage({ mode: 'circuit-halfopen', id });
}
}
}, 1000);
channel.onmessageerror = (ev) => {
console.error(ev);
};
channel.onmessage = ({ data: { mode, id, data } }) => __awaiter(void 0, void 0, void 0, function* () {
// console.log('child got message', mode, id, data);
switch (mode) {
case 'push-error':
if (!errorRecords[id])
errorRecords[id] = [];
errorRecords[id].push({ time: Date.now(), error: data.errorType });
if (metadata[id]) {
if (debounce[id])
clearImmediate(debounce[id]);
debounce[id] = setImmediate(() => openProcess(id));
}
else {
if (!metadataRequested[id]) {
channel.postMessage({ mode: 'metadata', id });
metadataRequested[id] = true;
}
return;
}
break;
case 'circuit-open':
openStates[id] = { state: CircuitBreakerState.Open, changed: Date.now() };
break;
case 'circuit-close':
delete openStates[id];
errorRecords[id] = [];
break;
case 'metadata':
metadata[id] = data;
openProcess(id);
break;
}
});
}
;