Complete V1 performance and OOM release gates
This commit is contained in:
156
web/app/src/testing/oom-fault-session.ts
Normal file
156
web/app/src/testing/oom-fault-session.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { OOM_FAULT_ERROR, type OOMFaultObservationIR, type OOMFaultPoint } from "../../../protocol/oom-recovery";
|
||||
|
||||
export interface OOMFaultConfiguration {
|
||||
point: OOMFaultPoint;
|
||||
failAfterBytes?: number;
|
||||
failAfterCount?: number;
|
||||
}
|
||||
|
||||
export interface OOMFaultSessionStats {
|
||||
active: boolean;
|
||||
currentBytes: number;
|
||||
peakBytes: number;
|
||||
liveResources: number;
|
||||
peakResources: number;
|
||||
releasedBytes: number;
|
||||
allocationCount: number;
|
||||
matchingAllocationCount: number;
|
||||
unauthorizedAttempts: number;
|
||||
triggered: boolean;
|
||||
}
|
||||
|
||||
export interface OOMAllocationLease {
|
||||
readonly id: string;
|
||||
readonly bytes: number;
|
||||
readonly tracked: boolean;
|
||||
release(): void;
|
||||
}
|
||||
|
||||
export class OOMFaultInjectedError extends Error {
|
||||
readonly name = "OOMFaultInjectedError";
|
||||
readonly observation: OOMFaultObservationIR;
|
||||
|
||||
constructor(observation: OOMFaultObservationIR) {
|
||||
super(`${observation.code}: deterministic allocation failure at ${observation.stage}`);
|
||||
this.observation = observation;
|
||||
}
|
||||
}
|
||||
|
||||
export class OOMFaultSessionAccessError extends Error {
|
||||
readonly name = "OOMFaultSessionAccessError";
|
||||
}
|
||||
|
||||
function threshold(value: number | undefined, label: string): number | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`OOM_FAULT_CONFIG_INVALID: ${label}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export class OOMFaultSession {
|
||||
private readonly token: string;
|
||||
private readonly configuration: OOMFaultConfiguration;
|
||||
private readonly resources = new Map<string, number>();
|
||||
private active = true;
|
||||
private currentBytes = 0;
|
||||
private peakBytes = 0;
|
||||
private peakResources = 0;
|
||||
private releasedBytes = 0;
|
||||
private allocationCount = 0;
|
||||
private matchingAllocationCount = 0;
|
||||
private matchingBytes = 0;
|
||||
private unauthorizedAttempts = 0;
|
||||
private triggered = false;
|
||||
|
||||
constructor(configuration: OOMFaultConfiguration, token: string) {
|
||||
if (!OOM_FAULT_ERROR[configuration.point]) throw new Error("OOM_FAULT_CONFIG_INVALID: point");
|
||||
const failAfterBytes = threshold(configuration.failAfterBytes, "failAfterBytes");
|
||||
const failAfterCount = threshold(configuration.failAfterCount, "failAfterCount");
|
||||
if (failAfterBytes === undefined && failAfterCount === undefined) throw new Error("OOM_FAULT_CONFIG_INVALID: threshold required");
|
||||
if (!/^[a-f0-9-]{36,128}$/.test(token)) throw new Error("OOM_FAULT_CONFIG_INVALID: token");
|
||||
this.configuration = { ...configuration, failAfterBytes, failAfterCount };
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
reserve(token: string, point: OOMFaultPoint, bytes: number, resourceId: string = crypto.randomUUID()): OOMAllocationLease {
|
||||
if (!this.active) return { id: resourceId, bytes, tracked: false, release: () => undefined };
|
||||
if (token !== this.token) {
|
||||
this.unauthorizedAttempts++;
|
||||
throw new OOMFaultSessionAccessError("OOM_FAULT_TOKEN_MISMATCH: fault session token is isolated");
|
||||
}
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || !resourceId || this.resources.has(resourceId)) throw new Error("OOM_FAULT_ALLOCATION_INVALID");
|
||||
this.allocationCount++;
|
||||
if (point === this.configuration.point) {
|
||||
const nextCount = this.matchingAllocationCount + 1;
|
||||
const nextBytes = this.matchingBytes + bytes;
|
||||
const countExceeded = this.configuration.failAfterCount !== undefined && this.matchingAllocationCount >= this.configuration.failAfterCount;
|
||||
const bytesExceeded = this.configuration.failAfterBytes !== undefined && nextBytes > this.configuration.failAfterBytes;
|
||||
this.matchingAllocationCount = nextCount;
|
||||
this.matchingBytes = nextBytes;
|
||||
if (countExceeded || bytesExceeded) {
|
||||
this.triggered = true;
|
||||
const mapping = OOM_FAULT_ERROR[point];
|
||||
throw new OOMFaultInjectedError({
|
||||
point,
|
||||
...mapping,
|
||||
attemptedBytes: bytes,
|
||||
allocationCount: nextCount,
|
||||
failAfterBytes: this.configuration.failAfterBytes,
|
||||
failAfterCount: this.configuration.failAfterCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.resources.set(resourceId, bytes);
|
||||
this.currentBytes += bytes;
|
||||
this.peakBytes = Math.max(this.peakBytes, this.currentBytes);
|
||||
this.peakResources = Math.max(this.peakResources, this.resources.size);
|
||||
let released = false;
|
||||
return {
|
||||
id: resourceId,
|
||||
bytes,
|
||||
tracked: true,
|
||||
release: () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
const allocated = this.resources.get(resourceId);
|
||||
if (allocated === undefined) return;
|
||||
this.resources.delete(resourceId);
|
||||
this.currentBytes -= allocated;
|
||||
this.releasedBytes += allocated;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
close(token: string): OOMFaultSessionStats {
|
||||
if (this.active && token !== this.token) {
|
||||
this.unauthorizedAttempts++;
|
||||
throw new OOMFaultSessionAccessError("OOM_FAULT_TOKEN_MISMATCH: fault session token is isolated");
|
||||
}
|
||||
this.active = false;
|
||||
return this.stats();
|
||||
}
|
||||
|
||||
stats(): OOMFaultSessionStats {
|
||||
return {
|
||||
active: this.active,
|
||||
currentBytes: this.currentBytes,
|
||||
peakBytes: this.peakBytes,
|
||||
liveResources: this.resources.size,
|
||||
peakResources: this.peakResources,
|
||||
releasedBytes: this.releasedBytes,
|
||||
allocationCount: this.allocationCount,
|
||||
matchingAllocationCount: this.matchingAllocationCount,
|
||||
unauthorizedAttempts: this.unauthorizedAttempts,
|
||||
triggered: this.triggered,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function beginOOMFaultSession(configuration: OOMFaultConfiguration): { session: OOMFaultSession; token: string } {
|
||||
const token = `${crypto.randomUUID()}-${crypto.randomUUID()}`;
|
||||
return { session: new OOMFaultSession(configuration, token), token };
|
||||
}
|
||||
|
||||
export function assertFault(error: unknown, point: OOMFaultPoint): OOMFaultObservationIR {
|
||||
if (!(error instanceof OOMFaultInjectedError) || error.observation.point !== point) throw error;
|
||||
return error.observation;
|
||||
}
|
||||
Reference in New Issue
Block a user