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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 16x 16x 16x 16x 14x 51x 51x 1x 1x 50x 50x 2x 2x 2x 2x 2x 2x 2x 50x 50x 50x 51x 56x 46x 46x 46x 56x 56x 56x 51x 21x 51x 51x 46x 46x 46x 46x 33x 32x 32x 32x 5x 5x 32x 53x 17x 24x 17x 17x 40x 48x 33x 16x 4x 4x 16x | /**
* SandboxStartSemaphore
*
* A lightweight counting semaphore used to limit Docker sandbox startup
* operations. It is acquired before docker run and released at RUNTIME_START.
* Waiters are served oldest first, except that while other subjects wait one
* subject holds at most half of the slots (work-conserving, no starvation).
*
* This prevents CPU starvation that occurs when many g++ processes compete for
* resources on the host machine. The semaphore is acquired before spawning a
* Docker container and released when the startup phase transitions to runtime
* (i.e. when [[RUNTIME_START]] is detected in stdout) or when the container
* exits with an error.
*
* Environment variables: SANDBOX_START_MAX_CONCURRENT (default 8) and
* SANDBOX_START_SLOT_TIMEOUT_MS (default 30000 ms)
*/
import { config } from "../../config";
export class SandboxStartSemaphore {
private readonly queue: Array<{ attempt: () => boolean; timer: NodeJS.Timeout; subject?: string }> = [];
private readonly activeBySubject = new Map<string, number>();
private _active = 0;
constructor(private readonly max: number) {}
/**
* Slots one subject may hold while other subjects wait. Work-conserving: with
* no one else waiting, a subject may use every free slot.
*/
private get subjectShare(): number {
return Math.max(1, Math.ceil(this.max / 2));
}
/**
* Acquire one sandbox-start slot.
*
* @param onQueued Optional callback invoked exactly once when this caller is
* placed in the queue (i.e. no slot is immediately available).
* @param signal Optional cancellation: an aborted waiter leaves the queue
* and never takes a slot.
* @param subject Optional owner; a released slot goes to the oldest waiter whose
* subject holds less than half of the slots before any other.
* @returns A release function. Must be called exactly once.
*/
acquire(onQueued?: () => void, timeoutMs = 60_000, signal?: AbortSignal, subject?: string): Promise<() => void> {
return new Promise<() => void>((resolve, reject) => {
if (signal?.aborted) {
reject(new Error("Sandbox start slot acquire cancelled"));
return;
}
let settled = false;
let attempt: () => boolean;
const leaveQueue = (error: Error) => {
Iif (settled) return;
const index = this.queue.findIndex((entry) => entry.attempt === attempt);
Eif (index !== -1) this.queue.splice(index, 1);
settled = true;
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
reject(error);
};
const onAbort = () => leaveQueue(new Error("Sandbox start slot acquire cancelled"));
const timer = setTimeout(
() => leaveQueue(new Error(`Sandbox start slot timeout after ${timeoutMs}ms`)),
timeoutMs,
);
signal?.addEventListener("abort", onAbort, { once: true });
attempt = () => {
if (settled || this._active >= this.max) return false;
settled = true;
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
this.take(subject);
resolve(this._makeRelease(subject));
return true;
};
if (this.queue.length === 0 && attempt()) return;
onQueued?.();
this.queue.push({ attempt, timer, subject });
this.grantWaiting();
});
}
private take(subject: string | undefined): void {
this._active++;
if (subject !== undefined) this.activeBySubject.set(subject, (this.activeBySubject.get(subject) ?? 0) + 1);
}
private _makeRelease(subject: string | undefined): () => void {
let released = false;
return () => {
if (released) return; // idempotent
released = true;
this._active--;
if (subject !== undefined) {
const remaining = (this.activeBySubject.get(subject) ?? 1) - 1;
if (remaining > 0) this.activeBySubject.set(subject, remaining);
else Ethis.activeBySubject.delete(subject);
}
this.grantWaiting();
};
}
/** Hands free slots to waiters: oldest within its subject's share first, otherwise oldest overall. */
private grantWaiting(): void {
while (this._active < this.max && this.queue.length > 0) {
const withinShare = this.queue.findIndex(({ subject }) =>
subject === undefined || (this.activeBySubject.get(subject) ?? 0) < this.subjectShare);
const [next] = this.queue.splice(withinShare === -1 ? 0 : withinShare, 1);
next?.attempt();
}
}
get activeCount(): number {
return this._active;
}
get queueLength(): number {
return this.queue.length;
}
}
// ─── Singleton factory ────────────────────────────────────────────────────────
let _instance: SandboxStartSemaphore | null = null;
/**
* Returns (or lazily creates) the global sandbox-start semaphore.
* Passing `maxOverride` replaces the configured value and resets the singleton.
*/
export function getSandboxStartSemaphore(maxOverride?: number): SandboxStartSemaphore {
if (maxOverride !== undefined || _instance === null) {
const max =
maxOverride ?? config.capacity.sandboxStartMaxConcurrent;
_instance = new SandboxStartSemaphore(max);
}
return _instance;
}
|