All files / server/services/sandbox docker-compile-semaphore.ts

78.04% Statements 32/41
73.68% Branches 14/19
81.81% Functions 9/11
86.11% Lines 31/36

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                                  8x 8x   8x                   20x 20x   20x               20x 27x 27x 20x 20x 20x 20x   7x 7x       20x         20x 20x 21x 20x 20x 20x 7x 7x           15x       17x           15x                   6x   2x 2x   6x    
/**
 * DockerCompileSemaphore
 *
 * A lightweight FIFO counting semaphore used to limit the number of Docker
 * containers that may be in the compile phase simultaneously.
 *
 * 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 compile phase transitions to runtime
 * (i.e. when [[RUNTIME_START]] is detected in stdout) or when the container
 * exits with an error.
 *
 * Environment variable: DOCKER_COMPILE_CONCURRENT (default 8)
 */
import { config } from "../../config";
 
export class DockerCompileSemaphore {
  private readonly queue: Array<{ attempt: () => void; timer: NodeJS.Timeout }> = [];
  private _active = 0;
 
  constructor(private readonly max: number) {}
 
  /**
   * Acquire one compile slot.
   *
   * @param onQueued  Optional callback invoked exactly once when this caller is
   *                  placed in the queue (i.e. no slot is immediately available).
   * @returns         A release function.  Must be called exactly once.
   */
  acquire(onQueued?: () => void, timeoutMs = 60_000): Promise<() => void> {
    return new Promise<() => void>((resolve, reject) => {
      let settled = false;
      let attempt: () => void;
      const timer = setTimeout(() => {
        if (settled) return;
        const index = this.queue.findIndex((entry) => entry.attempt === attempt);
        if (index !== -1) this.queue.splice(index, 1);
        settled = true;
        reject(new Error(`Docker compile slot timeout after ${timeoutMs}ms`));
      }, timeoutMs);
 
      attempt = () => {
        Iif (settled) return;
        if (this._active < this.max) {
          settled = true;
          clearTimeout(timer);
          this._active++;
          resolve(this._makeRelease());
        } else {
          onQueued?.();
          this.queue.push({ attempt, timer });
        }
      };
 
      attempt();
    });
  }
 
  private _makeRelease(): () => void {
    let released = false;
    return () => {
      if (released) return; // idempotent
      released = true;
      this._active--;
      if (this.queue.length > 0) {
        const next = this.queue.shift();
        next?.attempt();
      }
    };
  }
 
  get activeCount(): number {
    return this._active;
  }
 
  get queueLength(): number {
    return this.queue.length;
  }
}
 
// ─── Singleton factory ────────────────────────────────────────────────────────
 
let _instance: DockerCompileSemaphore | null = null;
 
/**
 * Returns (or lazily creates) the global DockerCompileSemaphore singleton.
 *
 * The concurrency limit is read from the DOCKER_COMPILE_CONCURRENT env var at
 * first call.  Passing `maxOverride` replaces the env-var value and resets the
 * singleton – useful in tests.
 */
export function getDockerCompileSemaphore(maxOverride?: number): DockerCompileSemaphore {
  if (maxOverride !== undefined || _instance === null) {
    const max =
      maxOverride ?? config.compilation.dockerCompileConcurrent;
    _instance = new DockerCompileSemaphore(max);
  }
  return _instance;
}