Files
workinf_Blender_Wasm/web/app/src/testing/oom-fault-session.ts
2026-08-14 22:32:09 -04:00

157 lines
5.8 KiB
TypeScript

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;
}