UNPKG

@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
/** * @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