mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
Add bounded weighted admission with fair queuing, deadline and cancellation handling, exact lease accounting, and a default-shadow runtime. Keep asynchronous resource-pressure shedding as an independent safety fuse and bound request feature estimation.
250 lines
7.5 KiB
TypeScript
250 lines
7.5 KiB
TypeScript
import { checkHeapPressureGuard, HEAP_PRESSURE_THRESHOLD_MB } from "./heapPressure.ts";
|
|
import { buildErrorBody } from "./error.ts";
|
|
import {
|
|
createResourcePressureTracker,
|
|
resolveResourcePressureThresholds,
|
|
type PressureReason,
|
|
type ResourcePressureState,
|
|
type ResourcePressureThresholds,
|
|
type ResourceSignals,
|
|
} from "./resourcePressurePolicy.ts";
|
|
import {
|
|
sampleResourceSignals,
|
|
type SampleResourceSignalsDeps,
|
|
} from "./resourcePressureSampler.ts";
|
|
|
|
const MB = 1024 * 1024;
|
|
const RETRY_AFTER_SECONDS = "5";
|
|
const PRESSURE_MESSAGE = "Service temporarily unavailable due to resource pressure. Retry shortly.";
|
|
|
|
export type ResourcePressureGuardResult = {
|
|
success: false;
|
|
status: 503;
|
|
error: string;
|
|
response: Response;
|
|
};
|
|
|
|
export type ResourcePressureObservation = {
|
|
signals: ResourceSignals | null;
|
|
state: ResourcePressureState;
|
|
};
|
|
|
|
export type ResourcePressureRuntimeOptions = {
|
|
thresholds?: Partial<ResourcePressureThresholds>;
|
|
heapThresholdMb?: number | null;
|
|
immediateHeapUsedMb?: () => number;
|
|
sample?: () => Promise<ResourceSignals>;
|
|
nowMs?: () => number;
|
|
schedule?: (refresh: () => void) => void;
|
|
staleAfterMs?: number;
|
|
maxStaleMs?: number;
|
|
retryAfterMs?: number;
|
|
samplerDeps?: SampleResourceSignalsDeps;
|
|
};
|
|
|
|
export type ResourcePressureRuntime = {
|
|
check: () => ResourcePressureGuardResult | null;
|
|
getObservation: () => ResourcePressureObservation;
|
|
whenRefreshSettled: () => Promise<void>;
|
|
dispose: () => void;
|
|
};
|
|
|
|
function emptyState(): ResourcePressureState {
|
|
return {
|
|
severity: "normal",
|
|
reason: "none",
|
|
elevatedStreak: 0,
|
|
recoveryStreak: 0,
|
|
lastTransitionAtMs: 0,
|
|
observedAtMs: 0,
|
|
};
|
|
}
|
|
|
|
function requireDuration(name: string, value: number): number {
|
|
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > 3_600_000) {
|
|
throw new RangeError(`${name} must be an integer between 0 and 3600000`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function buildCriticalGuard(reason: PressureReason): ResourcePressureGuardResult {
|
|
console.warn(
|
|
`[resourcePressure] critical pressure guard tripped (reason=${reason}); returning 503`
|
|
);
|
|
return {
|
|
success: false,
|
|
status: 503,
|
|
error: PRESSURE_MESSAGE,
|
|
response: new Response(
|
|
JSON.stringify(
|
|
buildErrorBody(503, PRESSURE_MESSAGE, undefined, {
|
|
type: "server_error",
|
|
code: "resource_pressure",
|
|
})
|
|
),
|
|
{
|
|
status: 503,
|
|
headers: { "Content-Type": "application/json", "Retry-After": RETRY_AFTER_SECONDS },
|
|
}
|
|
),
|
|
};
|
|
}
|
|
|
|
function immediateHeapGuard(
|
|
heapUsedMb: number,
|
|
thresholdMb: number | null
|
|
): ResourcePressureGuardResult | null {
|
|
if (thresholdMb == null) return null;
|
|
const guard = checkHeapPressureGuard(heapUsedMb, thresholdMb);
|
|
if (!guard) return null;
|
|
return buildCriticalGuard("v8_heap_absolute");
|
|
}
|
|
|
|
export function createResourcePressureRuntime(
|
|
options: ResourcePressureRuntimeOptions = {}
|
|
): ResourcePressureRuntime {
|
|
const heapThresholdMb =
|
|
options.heapThresholdMb === undefined ? HEAP_PRESSURE_THRESHOLD_MB : options.heapThresholdMb;
|
|
if (heapThresholdMb !== null && (!Number.isFinite(heapThresholdMb) || heapThresholdMb <= 0)) {
|
|
throw new RangeError("heapThresholdMb must be positive and finite or null");
|
|
}
|
|
const thresholds = resolveResourcePressureThresholds({
|
|
...options.thresholds,
|
|
heapAbsoluteThresholdMb:
|
|
options.thresholds?.heapAbsoluteThresholdMb === undefined
|
|
? null
|
|
: options.thresholds.heapAbsoluteThresholdMb,
|
|
});
|
|
const staleAfterMs = requireDuration("staleAfterMs", options.staleAfterMs ?? 1_000);
|
|
const maxStaleMs = requireDuration("maxStaleMs", options.maxStaleMs ?? 30_000);
|
|
const retryAfterMs = requireDuration("retryAfterMs", options.retryAfterMs ?? 1_000);
|
|
if (maxStaleMs < staleAfterMs) {
|
|
throw new RangeError("maxStaleMs must be greater than or equal to staleAfterMs");
|
|
}
|
|
|
|
const nowMs = options.nowMs ?? Date.now;
|
|
const immediateHeapUsedMb =
|
|
options.immediateHeapUsedMb ?? (() => process.memoryUsage().heapUsed / MB);
|
|
const sample = options.sample ?? (() => sampleResourceSignals(options.samplerDeps));
|
|
const schedule =
|
|
options.schedule ??
|
|
((refresh) => {
|
|
const handle = setImmediate(refresh);
|
|
handle.unref();
|
|
});
|
|
const tracker = createResourcePressureTracker(thresholds);
|
|
|
|
let lastSignals: ResourceSignals | null = null;
|
|
let state = emptyState();
|
|
let lastRefreshAtMs = Number.NEGATIVE_INFINITY;
|
|
let nextRefreshAtMs = Number.NEGATIVE_INFINITY;
|
|
let scheduled = false;
|
|
let inFlight: Promise<void> | null = null;
|
|
let disposed = false;
|
|
|
|
const refresh = (): void => {
|
|
if (disposed || inFlight) return;
|
|
scheduled = false;
|
|
inFlight = Promise.resolve()
|
|
.then(sample)
|
|
.then((signals) => {
|
|
if (disposed) return;
|
|
const settledAtMs = nowMs();
|
|
lastSignals = signals;
|
|
state = tracker.observe(signals);
|
|
lastRefreshAtMs = settledAtMs;
|
|
nextRefreshAtMs = settledAtMs + staleAfterMs;
|
|
})
|
|
.catch(() => {
|
|
if (!disposed) nextRefreshAtMs = nowMs() + retryAfterMs;
|
|
})
|
|
.finally(() => {
|
|
inFlight = null;
|
|
});
|
|
};
|
|
|
|
const scheduleRefresh = (): void => {
|
|
if (disposed || scheduled || inFlight) return;
|
|
scheduled = true;
|
|
schedule(refresh);
|
|
};
|
|
|
|
return {
|
|
check() {
|
|
let heapUsedMb = 0;
|
|
try {
|
|
heapUsedMb = immediateHeapUsedMb();
|
|
} catch {
|
|
heapUsedMb = 0;
|
|
}
|
|
const immediate = immediateHeapGuard(heapUsedMb, heapThresholdMb);
|
|
const now = nowMs();
|
|
if (now >= nextRefreshAtMs) scheduleRefresh();
|
|
if (immediate) {
|
|
state = {
|
|
severity: "critical",
|
|
reason: "v8_heap_absolute",
|
|
elevatedStreak: 0,
|
|
recoveryStreak: 0,
|
|
lastTransitionAtMs: now,
|
|
observedAtMs: now,
|
|
};
|
|
return immediate;
|
|
}
|
|
const cacheAge = lastSignals ? Math.max(0, now - lastRefreshAtMs) : Number.POSITIVE_INFINITY;
|
|
return cacheAge <= maxStaleMs && state.severity === "critical"
|
|
? buildCriticalGuard(state.reason)
|
|
: null;
|
|
},
|
|
getObservation: () => ({ signals: lastSignals, state }),
|
|
whenRefreshSettled: async () => {
|
|
if (scheduled) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
if (inFlight) await inFlight;
|
|
},
|
|
dispose() {
|
|
disposed = true;
|
|
scheduled = false;
|
|
},
|
|
};
|
|
}
|
|
|
|
let defaultRuntime = createResourcePressureRuntime();
|
|
|
|
export function checkResourcePressureGuard(): ResourcePressureGuardResult | null {
|
|
return defaultRuntime.check();
|
|
}
|
|
|
|
export function getResourcePressureObservation(): ResourcePressureObservation {
|
|
return defaultRuntime.getObservation();
|
|
}
|
|
|
|
/** Replaces and disposes the process singleton when configuration is reloaded. */
|
|
export function reloadResourcePressureRuntime(
|
|
options: ResourcePressureRuntimeOptions = {}
|
|
): ResourcePressureRuntime {
|
|
defaultRuntime.dispose();
|
|
defaultRuntime = createResourcePressureRuntime(options);
|
|
return defaultRuntime;
|
|
}
|
|
|
|
export type {
|
|
PressureReason,
|
|
PressureSeverity,
|
|
ResourceMetricBytes,
|
|
ResourcePressureState,
|
|
ResourcePressureThresholds,
|
|
ResourcePressureTracker,
|
|
ResourceSignals,
|
|
} from "./resourcePressurePolicy.ts";
|
|
export {
|
|
classifyAdaptiveResourcePressure as classifyResourcePressure,
|
|
createResourcePressureTracker,
|
|
resolveResourcePressureThresholds,
|
|
} from "./resourcePressurePolicy.ts";
|
|
export {
|
|
sampleResourceSignals,
|
|
sanitizeMemoryBytes,
|
|
type ResourcePressureFs,
|
|
type SampleResourceSignalsDeps,
|
|
} from "./resourcePressureSampler.ts";
|