@daisugi/kintsugi
Version:
Kintsugi is a set of utilities to help build a fault tolerant services.
87 lines • 3.14 kB
JavaScript
import { setInterval } from 'node:timers';
import { Result, } from '@daisugi/anzen';
import { Ayamari } from '@daisugi/ayamari';
const { errFn, errCode } = new Ayamari();
const defaultWindowDurationMs = 30000;
const defaultTotalBuckets = 10;
const defaultFailureThresholdRate = 50;
const defaultVolumeThreshold = 10;
const defaultReturnToServiceAfterMs = 5000;
const State = {
Close: 1,
Open: 2,
HalfOpen: 3,
};
const Measure = {
Failure: 1,
Calls: 2,
};
const circuitSuspendedErr = Result.failure(errFn.CircuitSuspended('Circuit suspended by circuit breaker.'));
export function isFailureResponse(response) {
if (response.isSuccess) {
return false;
}
if (response.isFailure &&
response.getError().code === errCode.NotFound) {
return false;
}
return true;
}
export function withCircuitBreaker(fn, opts = {}) {
const windowDurationMs = opts.windowDurationMs || defaultWindowDurationMs;
const totalBuckets = opts.totalBuckets || defaultTotalBuckets;
const failureThresholdRate = opts.failureThresholdRate ||
defaultFailureThresholdRate;
const volumeThreshold = opts.volumeThreshold || defaultVolumeThreshold;
const _isFailureResponse = opts.isFailureResponse || isFailureResponse;
const returnToServiceAfterMs = opts.returnToServiceAfterMs ||
defaultReturnToServiceAfterMs;
const buckets = [[0, 0]];
let currentState = State.Close;
let nextAttemptMs = Date.now();
setInterval(() => {
buckets.push([0, 0]);
if (buckets.length > totalBuckets) {
buckets.shift();
}
}, windowDurationMs / totalBuckets);
return async function (...args) {
if (currentState === State.Open) {
if (nextAttemptMs > Date.now()) {
return circuitSuspendedErr;
}
currentState = State.HalfOpen;
}
const response = await fn.apply(this, args);
const lastBucket = buckets[buckets.length - 1];
const isFailure = _isFailureResponse(response);
lastBucket[Measure.Calls] += 1;
if (isFailure) {
lastBucket[Measure.Failure] += 1;
}
let bucketsFailures = 0;
let bucketsCalls = 0;
for (const bucket of buckets) {
bucketsFailures += bucket[Measure.Failure];
bucketsCalls += bucket[Measure.Calls];
}
if (currentState === State.HalfOpen) {
const lastCallFailed = isFailure && bucketsCalls > volumeThreshold;
if (lastCallFailed) {
currentState = State.Open;
return circuitSuspendedErr;
}
currentState = State.Close;
return response;
}
const failuresRate = (bucketsFailures / bucketsCalls) * 100;
if (failuresRate > failureThresholdRate &&
bucketsCalls > volumeThreshold) {
currentState = State.Open;
nextAttemptMs = Date.now() + returnToServiceAfterMs;
return circuitSuspendedErr;
}
return response;
};
}
//# sourceMappingURL=with_circuit_breaker.js.map