Files
ridebus-status/server/reliability.mjs
2026-10-13 02:24:39 +03:00

64 lines
3.0 KiB
JavaScript

export function unionIntervals(intervals) {
const sorted = intervals.filter(([start, end]) => end > start).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const result = [];
for (const [start, end] of sorted) {
const last = result.at(-1);
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
else result.push([start, end]);
}
return result;
}
export function intersectIntervals(left, right) {
const a = unionIntervals(left), b = unionIntervals(right), result = [];
let i = 0, j = 0;
while (i < a.length && j < b.length) {
const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]);
if (end > start) result.push([start, end]);
if (a[i][1] <= b[j][1]) i++; else j++;
}
return result;
}
export function subtractIntervals(intervals, excluded) {
const result = [], exclusions = unionIntervals(excluded);
for (const [start, end] of unionIntervals(intervals)) {
let cursor = start;
for (const [a, b] of exclusions) {
if (b <= cursor) continue;
if (a >= end) break;
if (a > cursor) result.push([cursor, Math.min(a, end)]);
cursor = Math.max(cursor, b);
if (cursor >= end) break;
}
if (cursor < end) result.push([cursor, end]);
}
return result;
}
export const intervalDuration = intervals => unionIntervals(intervals).reduce((sum, [start, end]) => sum + end - start, 0);
export function reliabilitySummary({ availability, scenario, incidents, maintenance, since, now, lastRecovery }) {
const excluded = unionIntervals(maintenance);
const observed = subtractIntervals(scenario ? intersectIntervals(availability.known, scenario.known) : availability.known, excluded);
const outages = [];
let incomplete = false;
for (const incident of incidents) {
const expected = subtractIntervals([[Math.max(since, incident.startedAt), Math.min(now, incident.resolvedAt ?? now)]], excluded);
if (!expected.length) continue;
const data = incident.scope === 'scenario' ? scenario : availability;
const measured = intersectIntervals(expected, data?.down || []);
if (intervalDuration(intersectIntervals(expected, data?.known || [])) < intervalDuration(expected)) incomplete = true;
outages.push({ intervals: measured, duration: intervalDuration(measured) });
}
const measured = unionIntervals(outages.flatMap(item => item.intervals));
const duration = intervalDuration(measured);
const hasData = observed.length > 0 || duration > 0;
const usable = hasData && !(incomplete && duration === 0);
return {
downtimeMs: usable ? duration : null,
longestDowntimeMs: usable ? outages.reduce((longest, item) => Math.max(longest, item.duration), 0) : null,
incidentCount: outages.length, downtimeIncomplete: incomplete,
lastRecoveryAt: lastRecovery?.time ?? null, lastRecoveryScope: lastRecovery?.scope ?? null,
observedMs: intervalDuration(observed), coverage: now > since ? Number((intervalDuration(observed) / (now - since) * 100).toFixed(4)) : 0,
since, asOf: now,
};
}