@daiso-tech/core
Version:
The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.
171 lines • 6.42 kB
JavaScript
/**
* @module CircuitBreaker
*/
import { CLOSED_TRANSITIONS, HALF_OPEN_TRANSITIONS, } from "../../../../circuit-breaker/contracts/_module.js";
import { TO_MILLISECONDS, } from "../../../../time-span/contracts/_module.js";
import { TimeSpan } from "../../../../time-span/implementations/_module.js";
/**
* @internal
*/
function validateFailureThreshold(failureThreshold) {
if (Number.isInteger(failureThreshold)) {
throw new TypeError(`"SamplingBreakerSettings.failureThreshold" should be a float, got integer instead`);
}
if (failureThreshold <= 0 || failureThreshold >= 1) {
throw new RangeError(`"SamplingBreakerSettings.failureThreshold" should be between 0 and 1, got ${String(failureThreshold)}`);
}
}
/**
* @internal
*/
function validateSuccessThreshold(successThreshold) {
if (Number.isInteger(successThreshold)) {
throw new TypeError(`"SamplingBreakerSettings.successThreshold" should be a float, got integer instead`);
}
if (successThreshold <= 0 || successThreshold >= 1) {
throw new RangeError(`"SamplingBreakerSettings.successThreshold" should be between 0 and 1, got ${String(successThreshold)}`);
}
}
/**
* @internal
*/
export function resolveSamplingBreakerSettings(settings) {
const { failureThreshold = 0.2, successThreshold = 1 - failureThreshold, timeSpan = TimeSpan.fromMinutes(1), sampleTimeSpan = TimeSpan.fromTimeSpan(timeSpan).divide(6), minimumRps = 10, } = settings;
validateFailureThreshold(failureThreshold);
validateSuccessThreshold(successThreshold);
return {
failureThreshold,
successThreshold,
timeSpan,
sampleTimeSpan,
minimumRps,
};
}
/**
* @internal
*/
export function serializeSamplingBreakerSettings(settings) {
const { failureThreshold, successThreshold, timeSpan, sampleTimeSpan, minimumRps, } = resolveSamplingBreakerSettings(settings);
return {
failureThreshold,
successThreshold,
timeSpan: timeSpan[TO_MILLISECONDS](),
sampleTimeSpan: sampleTimeSpan[TO_MILLISECONDS](),
minimumRps,
};
}
/**
* The `SamplingBreaker` breaks after a proportion of requests over a time period fail.
*
* IMPORT_PATH: `"@daiso-tech/core/circuit-breaker/policies"`
* @group Policies
*/
export class SamplingBreaker {
failureThreshold;
successThreshold;
timeSpan;
sampleTimeSpan;
minimumRps;
constructor(settings = {}) {
const { failureThreshold, successThreshold, timeSpan, sampleTimeSpan, minimumRps, } = resolveSamplingBreakerSettings(settings);
this.failureThreshold = failureThreshold;
this.successThreshold = successThreshold;
this.timeSpan = TimeSpan.fromTimeSpan(timeSpan);
this.sampleTimeSpan = TimeSpan.fromTimeSpan(sampleTimeSpan);
this.minimumRps = minimumRps;
}
initialMetrics() {
return {
samples: [],
};
}
static getProccesedMetricData(currentMetrics) {
let totalFailures = 0;
let totalSuccesses = 0;
for (const sample of currentMetrics.samples) {
totalFailures += sample.failures;
totalSuccesses += sample.successes;
}
const total = totalFailures + totalSuccesses;
return {
total,
totalFailures,
totalSuccesses,
};
}
isMiniumNotMet(total) {
return total < Math.ceil(this.timeSpan.toSeconds() * this.minimumRps);
}
whenClosed(currentMetrics, _currentDate) {
const { total, totalFailures } = SamplingBreaker.getProccesedMetricData(currentMetrics);
if (this.isMiniumNotMet(total)) {
return CLOSED_TRANSITIONS.NONE;
}
const failureCount = Math.ceil(this.failureThreshold * total);
const hasFailed = totalFailures > failureCount;
if (hasFailed) {
return CLOSED_TRANSITIONS.TO_OPEN;
}
return CLOSED_TRANSITIONS.NONE;
}
whenHalfOpened(currentMetrics, _currentDate) {
const { total, totalSuccesses } = SamplingBreaker.getProccesedMetricData(currentMetrics);
if (this.isMiniumNotMet(total)) {
return HALF_OPEN_TRANSITIONS.NONE;
}
const successCount = Math.ceil(this.successThreshold * total);
const hasSucceeded = totalSuccesses > successCount;
if (hasSucceeded) {
return HALF_OPEN_TRANSITIONS.TO_CLOSED;
}
return HALF_OPEN_TRANSITIONS.TO_OPEN;
}
isNotOverlapping(currentDate) {
return (sample) => {
const windowStart = this.timeSpan.toStartDate(currentDate);
const sampleEnd = this.sampleTimeSpan.toEndDate(new Date(sample.startedAt));
return sampleEnd < windowStart;
};
}
track(success, currentState, settings) {
let samples = [...currentState.metrics.samples].filter(this.isNotOverlapping(settings.currentDate));
let currentSample = samples.at(-1) ?? {
failures: 0,
successes: 0,
startedAt: settings.currentDate.getTime(),
};
const currentSampleEnd = this.sampleTimeSpan
.toEndDate(settings.currentDate)
.getTime();
const hasCurrentSampleEnded = currentSampleEnd < settings.currentDate.getTime();
if (hasCurrentSampleEnded) {
currentSample = {
failures: 0,
successes: 0,
startedAt: settings.currentDate.getTime(),
};
samples = [...samples, currentSample];
}
if (success) {
currentSample.successes++;
}
else {
currentSample.failures++;
}
return {
samples,
};
}
trackFailure(currentState, settings) {
return this.track(false, currentState, settings);
}
trackSuccess(currentState, settings) {
return this.track(true, currentState, settings);
}
isEqual(metricsA, metricsB) {
const sortedMetricsA = [...metricsA.samples].sort((metrics1, metrics2) => metrics1.startedAt > metrics2.startedAt ? 1 : -1);
const sortedMetricsB = [...metricsB.samples].sort((metrics1, metrics2) => metrics1.startedAt > metrics2.startedAt ? 1 : -1);
return (JSON.stringify(sortedMetricsA) === JSON.stringify(sortedMetricsB));
}
}
//# sourceMappingURL=sampling-breaker.js.map