Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | 20x 59x 59x 59x 59x 59x 59x 141x 141x 3x 3x 138x 3x 3x 135x 135x 141x 141x 141x 141x 87x 87x 86x 86x 86x 86x 86x 86x 186x 20x 18x 18x | import { randomUUID } from "node:crypto";
import { config } from "../config";
export type SimulationReservation = Readonly<{
id: string;
subject: string;
}>;
export type AdmissionResult =
| { admitted: true; reservation: SimulationReservation }
| { admitted: false; reason: "identity" | "capacity" };
export const DEFAULT_MAX_SIMULATIONS_PER_SUBJECT = 5;
/**
* Process-local admission control. reserve() and release() mutate synchronously,
* so each decision is atomic within Node's event loop. Reservation IDs make a
* delayed release from an old run harmless.
*/
export class SimulationAdmissionController {
private readonly reservationsBySubject = new Map<string, Set<SimulationReservation>>();
private readonly reservationsById = new Map<string, SimulationReservation>();
private capacityRejectedTotal = 0;
private identityRejectedTotal = 0;
constructor(
private readonly maxReservations = config.capacity.admissionMax,
private readonly maxReservationsPerSubject = DEFAULT_MAX_SIMULATIONS_PER_SUBJECT,
) {}
reserve(subject: string): AdmissionResult {
const subjectReservations = this.reservationsBySubject.get(subject);
if ((subjectReservations?.size ?? 0) >= this.maxReservationsPerSubject) {
this.identityRejectedTotal++;
return { admitted: false, reason: "identity" };
}
if (this.reservationsById.size >= this.maxReservations) {
this.capacityRejectedTotal++;
return { admitted: false, reason: "capacity" };
}
const reservation = Object.freeze({ id: randomUUID(), subject });
const reservations = subjectReservations ?? new Set<SimulationReservation>();
reservations.add(reservation);
this.reservationsBySubject.set(subject, reservations);
this.reservationsById.set(reservation.id, reservation);
return { admitted: true, reservation };
}
release(reservation: SimulationReservation): boolean {
const current = this.reservationsById.get(reservation.id);
if (current !== reservation) return false;
const subjectReservations = this.reservationsBySubject.get(reservation.subject);
Iif (!subjectReservations?.has(reservation)) return false;
this.reservationsById.delete(reservation.id);
subjectReservations.delete(reservation);
if (subjectReservations.size === 0) this.reservationsBySubject.delete(reservation.subject);
return true;
}
getStats() {
return {
active: this.reservationsById.size,
max: this.maxReservations,
maxPerSubject: this.maxReservationsPerSubject,
capacityRejectedTotal: this.capacityRejectedTotal,
identityRejectedTotal: this.identityRejectedTotal,
};
}
}
let instance: SimulationAdmissionController | null = null;
export function getSimulationAdmissionController(): SimulationAdmissionController {
instance ??= new SimulationAdmissionController();
return instance;
}
|