mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
* feat(admission): per-target lane-aware probes for combo/fusion fan-out (#9654 Wave 2)
Combo and fusion fan out N targets without ever consulting the adaptive-admission
layer: the parent request holds one lease, but each fan-out target is dispatched
unconditionally. With virtual lanes enabled (OMNIROUTE_CHAT_VIRTUAL_LANES=1), a
connection whose lane queue is full now SKIPS additional fan-out targets instead
of piling more queued work onto an already-congested session.
Adds PerTargetAdmissionHook (admission/types.ts) + createPerTargetAdmissionHook
factory (chatAdmission.ts): strictly non-blocking (maxWaitMs 0 - skip, never
queue), a no-op when virtual lanes are off, keyed to the parent tenantKey, and
release-on-admit so the probe is a capacity gate, not a hold.
Threaded through every parallel fan-out path:
- priority/weighted executeTarget + round-robin skip chains (combo.ts)
- fusion panel before fan-out (fusion.ts), judge fallback prefers survivors
- chaos parallel panel (autoCombo/chaosEngine.ts)
- tryFusionDispatch / tryRuntimeUnitDispatch / buildBaseOptions (dispatchPrelude.ts)
- chat.ts primary + safety-net redirect call sites
Snapshot exposes virtualLanes so the no-op gate is cheap and honest.
Tests: tests/unit/combo-lane-awareness-9654.test.ts (10 tests) - factory
semantics, priority/RR skip, fusion panel drop + all-skipped 503, no-hook
backward-compat baseline.
* feat(flags): activation UX - env-wins adaptive virtual-lanes flag + env docs (#9654 Wave 2)
U7: make adaptive virtual admission lanes discoverable + activatable.
- New OMNIROUTE_CHAT_VIRTUAL_LANES feature flag (boolean/runtime/requiresRestart) in featureFlagDefinitions + en.json i18n key.
- lib/admissionVirtualLanes.ts: env-wins resolver (env > DB > default) + boot warm folding a DB-sourced override into the process-global runtime env via reloadAdaptiveAdmissionRuntime(options.env) - no process.env mutation, no open-sse changes. Env still wins; DB toggle gates at next boot.
- GET /api/settings/feature-flags special-cases the flag to report the gate true source (ccDiscoveryAliases precedent); flagPayload helper dedupes the payload shape.
- Wire the warm into instrumentation-node registerNodejs (non-fatal, DB-ready).
- Document the master switch in .env.example + ENVIRONMENT.md with the system-1/system-2 distinction; zero new env-doc-sync drift.
- 11 new tests (resolver precedence + warm); 60/60 across feature-flag suites; typecheck core clean; ESLint + doc gates green.
* feat(mcp): surface adaptive admission lane data in omniroute_get_health (#9654 Wave 2)
U8: make adaptive virtual-lane admission visible to agents via the MCP health tool. handleGetHealth now surfaces a curated adaptiveAdmission block from the health payload (which already carried the runtime snapshot but was dropping it): virtualLanes/pressure/utilization/laneCount/laneQueuedCount/laneQueuedCost, laneTenants capped at top-10 by queued cost, admitted/rejected/wouldReject counts, shutdown. Block omitted entirely when the health endpoint reports none.
isLaneFlagOn mirrors the runtime 1|true convention so a string serialization can never invert a boolean lane report. getHealthOutput schema extended with the matching optional shape; tool description updated.
4 new dispatch tests (full block, top-10 cap/order, omission, defensive coercion of string flags + malformed lane entries) - 22/22 in essentialTools.test.ts. README: Adaptive Admission Lane Data table + Skills & Tool Navigability audit (29/43 schema entries covered, 14 undocumented, tool_search keyword runtime discovery, full catalog in docs/frameworks/MCP-SERVER.md).
No new lint errors (4 pre-existing in server.ts), typecheck core clean, doc counts + fabricated-docs gates green.
* docs: add changelog entry for #9654 Wave 2 (#10039)
* fix(codeql): suppress js/insufficient-password-hash false positive in lane-key fingerprinting (#10039)
resolveSessionId sha256-hashes bearer/x-api-key/x-goog-api-key to derive a deterministic, non-reversible per-key lane-bucket ID for virtual admission lanes (#9654). This is not password storage or verification, so the rule is a false positive; suppress it inline (same house style as src/lib/sync/tokens.ts) to clear the codeqlAlerts ratchet (2 > baseline 1) that blocks #10039 and every PR against release/v3.8.50.
* docs(mcp): complete MCP server README tool reference (#10039)
The MCP server README covered only 29 of the 43 schema entries, listing the
remaining tools solely as a gap note with omniroute_tool_search as the runtime
fallback. Add tool-reference tables for the agent-skills trio, oneproxy trio,
web_fetch/web_search, tool_search, create_combo, set_routing_strategy,
pick_fastest_model, sync_pricing, and db_health_check so the README covers the
full schemas catalog, and fold the coverage note into the tool_search discovery
paragraph.
* fix(chat): drop unused correlationId from safety-net combo redirect (#10039)
handleComboChat's HandleComboChatOptions has no correlationId member and
the combo pipeline never consumes it; the property was copied from the
handleSingleModelChat options shape by accident and introduced a new
TS2353 under the open-sse workspace typecheck gate.
* fix(i18n): translate featureFlagChatVirtualLanesEnabledDescription into 42 locales (#10039)
en.json gained the flag description in this PR but the locale catalogs
were never mirrored, failing the pt-BR key-parity (#6695) and vi
completeness gates. Adds a real translation to every locale, keeping the
zh-CN/zh-TW glossary canonical terms (提供者/儀表板) and no ICU drift.
* chore(quality): ratchet open-sse-typecheck baseline down (#10039)
The Wave 2 admission refactor removed 66 baselined open-sse type errors;
re-freeze the baseline so the gate pins the new, tighter state.
* docs: resync provider reference to 341 and CLI tools to 34
The release branch gained an 11th no-auth provider (freeaiapikey registry
resync, #10233) and a 26th CLI Code tool without regenerating the
auto-generated docs, leaving every PR against release/v3.8.50 failing the
Docs Gates strict validator (code 341 vs doc 340, CLI 34 vs "33 tools").
Regenerate docs/reference/PROVIDER_REFERENCE.md and sync the provider/tool
counts across README.md, AGENTS.md, llm.txt plus 42 i18n mirrors,
package.json description, and the four diagram SVGs.
* fix(tests): align count expectations with live catalogs (pre-existing release drift)
Release/v3.8.50 currently fails five gates on its own tree; this PR inherits
them. Fix the stale expectations to match live code:
- feature-flags-settings: 48 -> 49 flags (Wave 2 adds OMNIROUTE_CHAT_VIRTUAL_LANES)
- cli-tools-schema / cli-catalog-counts: 33 -> 34 tools (zcode added; 26 code = 21 visible + 5 none)
- optional-transformers-dependency: onnxruntime-node ~1.24.3 -> ~1.27.0 (bump #10382)
- stryker.conf.json: register chatcore-header-drop-warn-dedupe-10315 test
- check-public-creds: freeze zcodeProtocol clientId false positive (client identifier, not a credential)
* fix(tests): follow release's onnxruntime-node revert to ~1.24.3
release/v3.8.50's #10543 pinned onnxruntime-node back to ~1.24.3 after
#10403's ~1.27.0 bump caused npm to nest a second native copy under
@huggingface/transformers and broke the Docker SONAME contract. This
PR's own drift-alignment commit (57b9c033) predates that revert and
still expected ~1.27.0; the 3-way merge did not flag it as a textual
conflict since only one side touched this exact line, but the merged
tree became internally inconsistent (package.json ~1.24.3 vs test
expecting ~1.27.0). Align the test with the now-canonical release
value.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(quality): dedupe stryker.conf.json chatcore-header-drop-warn-dedupe entry
The 3-way merge applied both sides' insertion of the same test-file entry
at different positions, producing a duplicate with broken indentation.
Adopted release's clean version of the file.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Brandon Bennett <branben@users.noreply.github.com>
627 lines
19 KiB
TypeScript
627 lines
19 KiB
TypeScript
/**
|
|
* Process-local adaptive admission runtime facade around the pure controller.
|
|
* No HTTP route wiring — suitable for later shared handleChat integration.
|
|
*/
|
|
|
|
import { AdaptiveAdmissionController } from "./controller.ts";
|
|
import { validateConfig } from "./config.ts";
|
|
import { extractAdmissionCostFeatures } from "./requestFeatures.ts";
|
|
import {
|
|
type AdaptiveAdmissionConfig,
|
|
type AdmissionAcquireResult,
|
|
type AdmissionClock,
|
|
type AdmissionLease,
|
|
type AdmissionMode,
|
|
type AdmissionPressure,
|
|
type AdmissionRejectCode,
|
|
type AdmissionReleaseOutcome,
|
|
type AdmissionSnapshot,
|
|
type ShadowDecision,
|
|
} from "./types.ts";
|
|
import { buildErrorBody } from "../../utils/error.ts";
|
|
import { CORS_HEADERS } from "../../utils/cors.ts";
|
|
import {
|
|
checkResourcePressureGuard,
|
|
getResourcePressureObservation,
|
|
type ResourcePressureGuardResult,
|
|
type ResourcePressureObservation,
|
|
} from "../../utils/resourcePressure.ts";
|
|
import type { PressureReason, PressureSeverity } from "../../utils/resourcePressurePolicy.ts";
|
|
|
|
export { extractAdmissionCostFeatures } from "./requestFeatures.ts";
|
|
|
|
export const DEFAULT_ADAPTIVE_ADMISSION_CONFIG: Readonly<AdaptiveAdmissionConfig> = Object.freeze({
|
|
mode: "shadow",
|
|
minLimit: 8,
|
|
initialLimit: 64,
|
|
maxLimit: 1000,
|
|
maxQueueCount: 128,
|
|
maxQueueCost: 2000,
|
|
defaultMaxWaitMs: 5_000,
|
|
windowMs: 1_000,
|
|
virtualLanes: false,
|
|
});
|
|
|
|
const RUNTIME_STORE_KEY = Symbol.for("omniroute.adaptiveAdmission.runtime");
|
|
|
|
type RuntimeStore = {
|
|
runtime: AdaptiveAdmissionRuntime | null;
|
|
};
|
|
|
|
type GlobalWithRuntimeStore = typeof globalThis & {
|
|
[RUNTIME_STORE_KEY]?: RuntimeStore;
|
|
};
|
|
|
|
function getRuntimeStore(): RuntimeStore {
|
|
const globalWithStore = globalThis as GlobalWithRuntimeStore;
|
|
let store = globalWithStore[RUNTIME_STORE_KEY];
|
|
if (!store) {
|
|
store = { runtime: null };
|
|
globalWithStore[RUNTIME_STORE_KEY] = store;
|
|
}
|
|
return store;
|
|
}
|
|
|
|
const ENV_KEYS = {
|
|
mode: "ADAPTIVE_ADMISSION_MODE",
|
|
minLimit: "ADAPTIVE_ADMISSION_MIN_LIMIT",
|
|
initialLimit: "ADAPTIVE_ADMISSION_INITIAL_LIMIT",
|
|
maxLimit: "ADAPTIVE_ADMISSION_MAX_LIMIT",
|
|
maxQueueCount: "ADAPTIVE_ADMISSION_MAX_QUEUE_COUNT",
|
|
maxQueueCost: "ADAPTIVE_ADMISSION_MAX_QUEUE_COST",
|
|
defaultMaxWaitMs: "ADAPTIVE_ADMISSION_MAX_WAIT_MS",
|
|
windowMs: "ADAPTIVE_ADMISSION_WINDOW_MS",
|
|
} as const;
|
|
|
|
function parsePositiveSafeInt(name: string, raw: string): number {
|
|
if (!/^[0-9]+$/.test(raw)) {
|
|
throw new RangeError(`${name} must be a positive safe integer`);
|
|
}
|
|
const value = Number(raw);
|
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
throw new RangeError(`${name} must be a positive safe integer`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/** Strict env → config resolver. Throws clear config errors for direct callers. */
|
|
export function resolveAdaptiveAdmissionConfigFromEnv(
|
|
env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env
|
|
): AdaptiveAdmissionConfig {
|
|
const cfg: AdaptiveAdmissionConfig = { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG };
|
|
|
|
const modeRaw = env[ENV_KEYS.mode];
|
|
if (modeRaw !== undefined && modeRaw !== "") {
|
|
if (modeRaw !== "off" && modeRaw !== "shadow" && modeRaw !== "enforce") {
|
|
throw new RangeError(`${ENV_KEYS.mode} must be off|shadow|enforce`);
|
|
}
|
|
cfg.mode = modeRaw;
|
|
}
|
|
|
|
// Numeric env keys only — typed assignment without index-signature cast (TS2352).
|
|
type EnvIntField = Exclude<keyof typeof ENV_KEYS, "mode">;
|
|
const intFields = [
|
|
"minLimit",
|
|
"initialLimit",
|
|
"maxLimit",
|
|
"maxQueueCount",
|
|
"maxQueueCost",
|
|
"defaultMaxWaitMs",
|
|
"windowMs",
|
|
] as const satisfies ReadonlyArray<EnvIntField>;
|
|
for (const field of intFields) {
|
|
const envName = ENV_KEYS[field];
|
|
const raw = env[envName];
|
|
if (raw === undefined || raw === "") continue;
|
|
cfg[field] = parsePositiveSafeInt(envName, raw);
|
|
}
|
|
|
|
// Shared pure validation — accept exact documented maxima, reject core-invalid configs.
|
|
validateConfig(cfg);
|
|
|
|
// Per-tenant virtual admission lanes (#9654) — opt-in via OMNIROUTE_CHAT_VIRTUAL_LANES.
|
|
const vlRaw = env.OMNIROUTE_CHAT_VIRTUAL_LANES;
|
|
cfg.virtualLanes = vlRaw === "1" || vlRaw === "true";
|
|
|
|
return cfg;
|
|
}
|
|
|
|
export type AdaptiveAdmissionAcquireInput = {
|
|
/** Opaque fairness key; never exposed in snapshots or client errors. */
|
|
tenantKey: string;
|
|
/** Already-parsed request body — must not be re-read or stringified for cost. */
|
|
body: unknown;
|
|
signal?: AbortSignal;
|
|
maxWaitMs?: number;
|
|
/** Authoritative streaming class; wins body stream inference when set. */
|
|
streaming?: boolean;
|
|
};
|
|
|
|
export type AdaptiveAdmissionAdmitted = {
|
|
status: "admitted";
|
|
mode: AdmissionMode;
|
|
lease: AdmissionLease;
|
|
admittedAtMs: number;
|
|
shadowDecision?: ShadowDecision;
|
|
};
|
|
|
|
export type AdaptiveAdmissionRejected = {
|
|
status: "rejected";
|
|
code: string;
|
|
response: Response;
|
|
};
|
|
|
|
export type AdaptiveAdmissionAcquireResult = AdaptiveAdmissionAdmitted | AdaptiveAdmissionRejected;
|
|
|
|
export type AdaptiveAdmissionPublicSnapshot = AdmissionSnapshot & {
|
|
resourceSeverity: PressureSeverity;
|
|
resourceReason: PressureReason;
|
|
resourceObservedAtMs: number;
|
|
pressureGuardRejectCount: number;
|
|
};
|
|
|
|
export type AdaptiveAdmissionLifecycleOptions = {
|
|
admittedAtMs: number;
|
|
signal?: AbortSignal;
|
|
nowMs?: () => number;
|
|
};
|
|
|
|
export type AdaptiveAdmissionRuntimeOptions = {
|
|
config?: AdaptiveAdmissionConfig;
|
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
clock?: Partial<AdmissionClock>;
|
|
checkResourcePressure?: () => ResourcePressureGuardResult | null;
|
|
getResourcePressureObservation?: () => ResourcePressureObservation;
|
|
/** Test seam: observe pressure values fed into the controller after dedupe. */
|
|
onPressureObserved?: (pressure: AdmissionPressure) => void;
|
|
warn?: (message: string) => void;
|
|
nowMs?: () => number;
|
|
};
|
|
|
|
/** Non-success release outcomes callers must choose explicitly for handler failures. */
|
|
export type AdaptiveAdmissionFailureOutcome = Exclude<AdmissionReleaseOutcome, "success">;
|
|
|
|
export type AdaptiveAdmissionRuntime = {
|
|
acquire(input: AdaptiveAdmissionAcquireInput): Promise<AdaptiveAdmissionAcquireResult>;
|
|
snapshot(): AdaptiveAdmissionPublicSnapshot;
|
|
dispose(): void;
|
|
/**
|
|
* Release an admitted lease after a handler failure before any HTTP response exists.
|
|
* Callers must supply the concrete non-success outcome — never defaults to local_reject.
|
|
*/
|
|
releaseHandlerFailure(
|
|
lease: AdmissionLease,
|
|
outcome: AdaptiveAdmissionFailureOutcome,
|
|
options?: { admittedAtMs?: number; nowMs?: () => number }
|
|
): void;
|
|
attachResponseLifecycle(
|
|
response: Response,
|
|
lease: AdmissionLease,
|
|
options: AdaptiveAdmissionLifecycleOptions
|
|
): Response;
|
|
};
|
|
|
|
type RejectHttpMapping = {
|
|
status: number;
|
|
code: string;
|
|
message: string;
|
|
retryAfter?: string;
|
|
};
|
|
|
|
const REJECT_MAP: Record<AdmissionRejectCode, RejectHttpMapping> = {
|
|
ADMISSION_ABORTED: {
|
|
status: 499,
|
|
code: "admission_aborted",
|
|
message: "Request aborted",
|
|
},
|
|
ADMISSION_OVERSIZED: {
|
|
status: 503,
|
|
code: "admission_oversized",
|
|
message: "Request too large for current capacity",
|
|
},
|
|
ADMISSION_QUEUE_FULL: {
|
|
status: 503,
|
|
code: "admission_queue_full",
|
|
message: "Service temporarily unavailable",
|
|
retryAfter: "1",
|
|
},
|
|
ADMISSION_DEADLINE: {
|
|
status: 503,
|
|
code: "admission_deadline",
|
|
message: "Service temporarily unavailable",
|
|
retryAfter: "1",
|
|
},
|
|
ADMISSION_SHUTDOWN: {
|
|
status: 503,
|
|
code: "admission_shutdown",
|
|
message: "Service temporarily unavailable",
|
|
},
|
|
ADMISSION_UNAVAILABLE: {
|
|
status: 503,
|
|
code: "admission_unavailable",
|
|
message: "Service temporarily unavailable",
|
|
retryAfter: "1",
|
|
},
|
|
ADMISSION_LANE_EVICTED: {
|
|
status: 503,
|
|
code: "admission_lane_evicted",
|
|
message: "Connection lane evicted",
|
|
retryAfter: "1",
|
|
},
|
|
};
|
|
|
|
function isAdmissionRejectError(
|
|
err: unknown
|
|
): err is { code: AdmissionRejectCode; name: string; message: string } {
|
|
return (
|
|
!!err &&
|
|
typeof err === "object" &&
|
|
(err as { name?: string }).name === "AdmissionRejectError" &&
|
|
typeof (err as { code?: unknown }).code === "string"
|
|
);
|
|
}
|
|
|
|
function buildAdmissionRejectResponse(code: AdmissionRejectCode): AdaptiveAdmissionRejected {
|
|
const mapping = REJECT_MAP[code] ?? REJECT_MAP.ADMISSION_UNAVAILABLE;
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
...CORS_HEADERS,
|
|
};
|
|
if (mapping.retryAfter) headers["Retry-After"] = mapping.retryAfter;
|
|
const body = buildErrorBody(mapping.status, mapping.message, undefined, {
|
|
type: mapping.status === 499 ? "client_disconnected" : "server_error",
|
|
code: mapping.code,
|
|
});
|
|
return {
|
|
status: "rejected",
|
|
code: mapping.code,
|
|
response: new Response(JSON.stringify(body), {
|
|
status: mapping.status,
|
|
headers,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function observationIdentity(state: ResourcePressureObservation["state"]): string {
|
|
return `${state.observedAtMs}|${state.severity}|${state.reason}`;
|
|
}
|
|
|
|
function toAdmissionPressure(severity: PressureSeverity): AdmissionPressure {
|
|
if (severity === "critical") return "critical";
|
|
if (severity === "high") return "high";
|
|
return "normal";
|
|
}
|
|
|
|
function isSseResponse(response: Response): boolean {
|
|
const contentType = response.headers.get("content-type") ?? "";
|
|
return contentType.toLowerCase().includes("text/event-stream");
|
|
}
|
|
|
|
function releaseOnce(
|
|
lease: AdmissionLease,
|
|
outcome: AdmissionReleaseOutcome,
|
|
admittedAtMs: number | undefined,
|
|
nowMs: () => number
|
|
): void {
|
|
if (lease.released) return;
|
|
const latencyMs = admittedAtMs === undefined ? undefined : Math.max(0, nowMs() - admittedAtMs);
|
|
lease.release(outcome, latencyMs === undefined ? undefined : { latencyMs });
|
|
}
|
|
|
|
/**
|
|
* Map HTTP status (+ optional request signal) to admission release outcome.
|
|
* Cancellation always wins over status classification.
|
|
*/
|
|
function classifyHttpOutcome(status: number, signal?: AbortSignal): AdmissionReleaseOutcome {
|
|
if (signal?.aborted || status === 499) return "cancelled";
|
|
if (status === 408 || status === 504) return "timeout";
|
|
if (status >= 500) return "upstream_error";
|
|
if (status >= 400) return "local_reject";
|
|
// 2xx / 3xx (and rare 1xx) complete successfully from admission's perspective.
|
|
return "success";
|
|
}
|
|
|
|
class AdaptiveAdmissionRuntimeImpl implements AdaptiveAdmissionRuntime {
|
|
private readonly controller: AdaptiveAdmissionController;
|
|
private readonly checkResourcePressure: () => ResourcePressureGuardResult | null;
|
|
private readonly getResourcePressureObservation: () => ResourcePressureObservation;
|
|
private readonly onPressureObserved?: (pressure: AdmissionPressure) => void;
|
|
private readonly nowMs: () => number;
|
|
private lastObservationKey: string | null = null;
|
|
private lastResource: {
|
|
severity: PressureSeverity;
|
|
reason: PressureReason;
|
|
observedAtMs: number;
|
|
} = { severity: "normal", reason: "none", observedAtMs: 0 };
|
|
private pressureGuardRejectCount = 0;
|
|
private disposed = false;
|
|
|
|
constructor(options: AdaptiveAdmissionRuntimeOptions, config: AdaptiveAdmissionConfig) {
|
|
this.controller = new AdaptiveAdmissionController(config, options.clock);
|
|
this.checkResourcePressure = options.checkResourcePressure ?? checkResourcePressureGuard;
|
|
this.getResourcePressureObservation =
|
|
options.getResourcePressureObservation ?? getResourcePressureObservation;
|
|
this.onPressureObserved = options.onPressureObserved;
|
|
this.nowMs = options.nowMs ?? options.clock?.now ?? (() => Date.now());
|
|
}
|
|
|
|
async acquire(input: AdaptiveAdmissionAcquireInput): Promise<AdaptiveAdmissionAcquireResult> {
|
|
if (this.disposed) {
|
|
return buildAdmissionRejectResponse("ADMISSION_SHUTDOWN");
|
|
}
|
|
|
|
// Independent safety fuse first — never acquire provider work on critical guard.
|
|
// Still feed pressure observations so the controller learns from critical samples.
|
|
let guard: ResourcePressureGuardResult | null = null;
|
|
try {
|
|
guard = this.checkResourcePressure();
|
|
} catch {
|
|
// Fail open on sampling/check failures.
|
|
}
|
|
|
|
this.feedFreshPressureObservation();
|
|
|
|
if (guard) {
|
|
this.pressureGuardRejectCount += 1;
|
|
return {
|
|
status: "rejected",
|
|
code: "resource_pressure",
|
|
response: guard.response,
|
|
};
|
|
}
|
|
|
|
const features = extractAdmissionCostFeatures(
|
|
input.body,
|
|
input.streaming === undefined ? undefined : { streaming: input.streaming }
|
|
);
|
|
let result: AdmissionAcquireResult;
|
|
try {
|
|
result = await this.controller.acquire({
|
|
tenantKey: input.tenantKey,
|
|
features,
|
|
signal: input.signal,
|
|
maxWaitMs: input.maxWaitMs,
|
|
});
|
|
} catch (err) {
|
|
if (isAdmissionRejectError(err)) {
|
|
return buildAdmissionRejectResponse(err.code);
|
|
}
|
|
return buildAdmissionRejectResponse("ADMISSION_UNAVAILABLE");
|
|
}
|
|
|
|
if (result.status === "rejected") {
|
|
return buildAdmissionRejectResponse(result.code);
|
|
}
|
|
|
|
if (result.status === "queued") {
|
|
try {
|
|
const admitted = await result.promise;
|
|
return {
|
|
status: "admitted",
|
|
mode: this.controller.snapshot().mode,
|
|
lease: admitted.lease,
|
|
admittedAtMs: this.nowMs(),
|
|
shadowDecision: admitted.shadowDecision,
|
|
};
|
|
} catch (err) {
|
|
if (isAdmissionRejectError(err)) {
|
|
return buildAdmissionRejectResponse(err.code);
|
|
}
|
|
return buildAdmissionRejectResponse("ADMISSION_UNAVAILABLE");
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: "admitted",
|
|
mode: this.controller.snapshot().mode,
|
|
lease: result.lease,
|
|
admittedAtMs: this.nowMs(),
|
|
shadowDecision: result.shadowDecision,
|
|
};
|
|
}
|
|
|
|
snapshot(): AdaptiveAdmissionPublicSnapshot {
|
|
const core = this.controller.snapshot();
|
|
return {
|
|
...core,
|
|
resourceSeverity: this.lastResource.severity,
|
|
resourceReason: this.lastResource.reason,
|
|
resourceObservedAtMs: this.lastResource.observedAtMs,
|
|
pressureGuardRejectCount: this.pressureGuardRejectCount,
|
|
};
|
|
}
|
|
|
|
dispose(): void {
|
|
if (this.disposed) return;
|
|
this.disposed = true;
|
|
this.controller.shutdown();
|
|
}
|
|
|
|
releaseHandlerFailure(
|
|
lease: AdmissionLease,
|
|
outcome: AdaptiveAdmissionFailureOutcome,
|
|
options?: { admittedAtMs?: number; nowMs?: () => number }
|
|
): void {
|
|
releaseOnce(lease, outcome, options?.admittedAtMs, options?.nowMs ?? this.nowMs);
|
|
}
|
|
|
|
attachResponseLifecycle(
|
|
response: Response,
|
|
lease: AdmissionLease,
|
|
options: AdaptiveAdmissionLifecycleOptions
|
|
): Response {
|
|
const nowMs = options.nowMs ?? this.nowMs;
|
|
const admittedAtMs = options.admittedAtMs;
|
|
|
|
if (!response.body || !isSseResponse(response)) {
|
|
releaseOnce(lease, classifyHttpOutcome(response.status, options.signal), admittedAtMs, nowMs);
|
|
return response;
|
|
}
|
|
|
|
const upstream = response.body;
|
|
const reader = upstream.getReader();
|
|
let settled = false;
|
|
let readerCancelled = false;
|
|
|
|
const settle = (outcome: AdmissionReleaseOutcome): void => {
|
|
if (settled) return;
|
|
settled = true;
|
|
releaseOnce(lease, outcome, admittedAtMs, nowMs);
|
|
};
|
|
|
|
const cancelReader = (reason?: unknown): void => {
|
|
if (readerCancelled) return;
|
|
readerCancelled = true;
|
|
void reader.cancel(reason).catch(() => {
|
|
/* ignore cancel races */
|
|
});
|
|
};
|
|
|
|
const onAbort = (): void => {
|
|
cancelReader(options.signal?.reason);
|
|
settle("cancelled");
|
|
};
|
|
|
|
if (options.signal) {
|
|
if (options.signal.aborted) {
|
|
onAbort();
|
|
} else {
|
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
}
|
|
}
|
|
|
|
const detachAbort = (): void => {
|
|
options.signal?.removeEventListener("abort", onAbort);
|
|
};
|
|
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
if (settled) {
|
|
controller.close();
|
|
return;
|
|
}
|
|
try {
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
detachAbort();
|
|
settle(classifyHttpOutcome(response.status, options.signal));
|
|
controller.close();
|
|
return;
|
|
}
|
|
controller.enqueue(value);
|
|
} catch (err) {
|
|
detachAbort();
|
|
settle(options.signal?.aborted ? "cancelled" : "upstream_error");
|
|
controller.error(err);
|
|
}
|
|
},
|
|
cancel(reason) {
|
|
detachAbort();
|
|
cancelReader(reason);
|
|
settle("cancelled");
|
|
},
|
|
});
|
|
|
|
return new Response(stream, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: response.headers,
|
|
});
|
|
}
|
|
|
|
private feedFreshPressureObservation(): void {
|
|
try {
|
|
const observation = this.getResourcePressureObservation();
|
|
const state = observation.state;
|
|
this.lastResource = {
|
|
severity: state.severity,
|
|
reason: state.reason,
|
|
observedAtMs: state.observedAtMs,
|
|
};
|
|
const key = observationIdentity(state);
|
|
if (state.observedAtMs <= 0) return;
|
|
if (key === this.lastObservationKey) return;
|
|
this.lastObservationKey = key;
|
|
const pressure = toAdmissionPressure(state.severity);
|
|
this.controller.observePressure(pressure);
|
|
this.onPressureObserved?.(pressure);
|
|
} catch {
|
|
// Fail open.
|
|
}
|
|
}
|
|
}
|
|
|
|
function createRuntimeFromResolvedConfig(
|
|
options: AdaptiveAdmissionRuntimeOptions,
|
|
config: AdaptiveAdmissionConfig
|
|
): AdaptiveAdmissionRuntime {
|
|
return new AdaptiveAdmissionRuntimeImpl(options, config);
|
|
}
|
|
|
|
/**
|
|
* Create an injected adaptive-admission runtime for tests or process use.
|
|
* Invalid explicit `config` still throws (direct callers want fail-fast).
|
|
*/
|
|
export function createAdaptiveAdmissionRuntime(
|
|
options: AdaptiveAdmissionRuntimeOptions = {}
|
|
): AdaptiveAdmissionRuntime {
|
|
const config =
|
|
options.config ??
|
|
(options.env
|
|
? resolveAdaptiveAdmissionConfigFromEnv(options.env)
|
|
: { ...DEFAULT_ADAPTIVE_ADMISSION_CONFIG });
|
|
return createRuntimeFromResolvedConfig(options, config);
|
|
}
|
|
|
|
function warnInvalidDefaultConfig(warn: ((message: string) => void) | undefined): void {
|
|
const message =
|
|
"[adaptiveAdmission] invalid environment configuration; using default shadow admission settings";
|
|
if (warn) {
|
|
warn(message);
|
|
return;
|
|
}
|
|
console.warn(message);
|
|
}
|
|
|
|
function createDefaultProcessRuntime(
|
|
options: AdaptiveAdmissionRuntimeOptions = {}
|
|
): AdaptiveAdmissionRuntime {
|
|
const warn = options.warn;
|
|
try {
|
|
const config =
|
|
options.config ?? resolveAdaptiveAdmissionConfigFromEnv(options.env ?? process.env);
|
|
return createRuntimeFromResolvedConfig(options, config);
|
|
} catch {
|
|
warnInvalidDefaultConfig(warn);
|
|
return createRuntimeFromResolvedConfig(options, {
|
|
...DEFAULT_ADAPTIVE_ADMISSION_CONFIG,
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Call-time process-global runtime (HMR-safe via globalThis symbol store). */
|
|
export function getAdaptiveAdmissionRuntime(): AdaptiveAdmissionRuntime {
|
|
const store = getRuntimeStore();
|
|
if (!store.runtime) {
|
|
store.runtime = createDefaultProcessRuntime();
|
|
}
|
|
return store.runtime;
|
|
}
|
|
|
|
/** Dispose previous controller and replace the process-global runtime. */
|
|
export function reloadAdaptiveAdmissionRuntime(
|
|
options: AdaptiveAdmissionRuntimeOptions = {}
|
|
): AdaptiveAdmissionRuntime {
|
|
const store = getRuntimeStore();
|
|
store.runtime?.dispose();
|
|
store.runtime = createDefaultProcessRuntime(options);
|
|
return store.runtime;
|
|
}
|
|
|
|
/** Test isolation: dispose and clear the process-global runtime slot. */
|
|
export function resetAdaptiveAdmissionRuntimeForTests(): void {
|
|
const store = getRuntimeStore();
|
|
store.runtime?.dispose();
|
|
store.runtime = null;
|
|
}
|