Initial Commit
This commit is contained in:
209
packages/devtools/tooling/sandbox/src/pool/pool.ts
Normal file
209
packages/devtools/tooling/sandbox/src/pool/pool.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { SANDBOX_CODES } from "../error-codes/error-codes.js";
|
||||
import { recordPoolAcquisition } from "../telemetry/telemetry.js";
|
||||
import type {
|
||||
Clock,
|
||||
ExecutionContext,
|
||||
SandboxEngine,
|
||||
SandboxEngineConfig,
|
||||
SandboxOptions,
|
||||
SandboxResult,
|
||||
} from "../types/sandbox.types.js";
|
||||
|
||||
/** Factory that builds engines on demand. */
|
||||
export type EngineFactory = (config: SandboxEngineConfig) => SandboxEngine;
|
||||
|
||||
/** Pool configuration. */
|
||||
export interface SandboxPoolConfig {
|
||||
readonly minSize: number;
|
||||
readonly maxSize: number;
|
||||
readonly engineConfig: SandboxEngineConfig;
|
||||
readonly factory: EngineFactory;
|
||||
readonly idleTimeoutMs: number;
|
||||
readonly maxExecutionsPerEngine: number;
|
||||
readonly clock?: Clock;
|
||||
}
|
||||
|
||||
interface PooledEngine {
|
||||
engine: SandboxEngine;
|
||||
inUse: boolean;
|
||||
executionCount: number;
|
||||
lastUsed: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
const DEFAULTS: Omit<SandboxPoolConfig, "engineConfig" | "factory"> = {
|
||||
minSize: 2,
|
||||
maxSize: 10,
|
||||
idleTimeoutMs: 60_000,
|
||||
maxExecutionsPerEngine: 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Pool of pre-warmed sandbox engines. The pool grows lazily up to `maxSize`,
|
||||
* recycles engines past `maxExecutionsPerEngine`, and reclaims idle engines
|
||||
* above `minSize` whose `lastUsed` exceeds `idleTimeoutMs` (relative to the
|
||||
* injectable {@link Clock}, defaulting to `Date.now`).
|
||||
*/
|
||||
export class SandboxPool {
|
||||
private readonly config: SandboxPoolConfig;
|
||||
private readonly clock: Clock;
|
||||
private pool: PooledEngine[] = [];
|
||||
|
||||
public constructor(
|
||||
config: Pick<SandboxPoolConfig, "engineConfig" | "factory"> &
|
||||
Partial<SandboxPoolConfig>
|
||||
) {
|
||||
this.config = { ...DEFAULTS, ...config };
|
||||
this.clock = this.config.clock ?? (() => Date.now());
|
||||
this.warmUp();
|
||||
}
|
||||
|
||||
/** Execute code on a pooled engine. */
|
||||
public async execute<T = unknown>(
|
||||
code: string,
|
||||
context?: ExecutionContext,
|
||||
options?: SandboxOptions
|
||||
): Promise<SandboxResult<T>> {
|
||||
const pooled = this.acquire();
|
||||
if (pooled === null) {
|
||||
recordPoolAcquisition("exhausted");
|
||||
return {
|
||||
success: false,
|
||||
result: undefined,
|
||||
error: `Sandbox pool exhausted (${SANDBOX_CODES.POOL_EXHAUSTED})`,
|
||||
duration: 0,
|
||||
memoryUsed: 0,
|
||||
logs: [],
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await pooled.engine.execute<T>(
|
||||
code,
|
||||
context,
|
||||
options
|
||||
);
|
||||
pooled.executionCount += 1;
|
||||
pooled.lastUsed = this.clock();
|
||||
return result;
|
||||
} finally {
|
||||
this.release(pooled);
|
||||
}
|
||||
}
|
||||
|
||||
/** Current pool size. */
|
||||
public size(): number {
|
||||
return this.pool.length;
|
||||
}
|
||||
|
||||
/** Idle engines. */
|
||||
public available(): number {
|
||||
return this.pool.filter((p) => !p.inUse).length;
|
||||
}
|
||||
|
||||
/** In-use engines. */
|
||||
public inUse(): number {
|
||||
return this.pool.filter((p) => p.inUse).length;
|
||||
}
|
||||
|
||||
/** Aggregate pool stats. */
|
||||
public stats(): {
|
||||
size: number;
|
||||
available: number;
|
||||
inUse: number;
|
||||
totalExecutions: number;
|
||||
} {
|
||||
let totalExecutions = 0;
|
||||
for (const p of this.pool) totalExecutions += p.executionCount;
|
||||
return {
|
||||
size: this.pool.length,
|
||||
available: this.available(),
|
||||
inUse: this.inUse(),
|
||||
totalExecutions,
|
||||
};
|
||||
}
|
||||
|
||||
/** Bring the pool up to `minSize`. */
|
||||
public warmUp(): void {
|
||||
while (this.pool.length < this.config.minSize) {
|
||||
this.pool.push(this.createPooled());
|
||||
}
|
||||
}
|
||||
|
||||
/** Single cleanup tick — reclaims idle engines above `minSize`. */
|
||||
public cleanupTick(): void {
|
||||
const now = this.clock();
|
||||
const survivors: PooledEngine[] = [];
|
||||
let removed = 0;
|
||||
for (const pooled of this.pool) {
|
||||
if (
|
||||
!pooled.inUse &&
|
||||
this.pool.length - removed > this.config.minSize &&
|
||||
now - pooled.lastUsed > this.config.idleTimeoutMs
|
||||
) {
|
||||
pooled.engine.destroy();
|
||||
removed += 1;
|
||||
continue;
|
||||
}
|
||||
survivors.push(pooled);
|
||||
}
|
||||
this.pool = survivors;
|
||||
}
|
||||
|
||||
/** Destroy every engine in the pool. */
|
||||
public destroy(): void {
|
||||
for (const pooled of this.pool) {
|
||||
pooled.engine.destroy();
|
||||
}
|
||||
this.pool = [];
|
||||
}
|
||||
|
||||
private acquire(): PooledEngine | null {
|
||||
const idle = this.pool.find((p) => !p.inUse);
|
||||
if (idle !== undefined) {
|
||||
idle.inUse = true;
|
||||
recordPoolAcquisition("acquired");
|
||||
return idle;
|
||||
}
|
||||
if (this.pool.length < this.config.maxSize) {
|
||||
const pooled = this.createPooled();
|
||||
pooled.inUse = true;
|
||||
this.pool.push(pooled);
|
||||
recordPoolAcquisition("created");
|
||||
return pooled;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private release(pooled: PooledEngine): void {
|
||||
pooled.inUse = false;
|
||||
if (pooled.executionCount >= this.config.maxExecutionsPerEngine) {
|
||||
this.recycle(pooled);
|
||||
}
|
||||
}
|
||||
|
||||
private recycle(pooled: PooledEngine): void {
|
||||
pooled.engine.destroy();
|
||||
// Replace in-place: copy a fresh engine onto the same PooledEngine
|
||||
// wrapper so the array reference stays valid for any other code
|
||||
// that holds it. We rotate every field so the recycled entry is
|
||||
// indistinguishable from a freshly created one.
|
||||
const fresh = this.createPooled();
|
||||
pooled.engine = fresh.engine;
|
||||
pooled.executionCount = 0;
|
||||
pooled.lastUsed = fresh.lastUsed;
|
||||
pooled.createdAt = fresh.createdAt;
|
||||
}
|
||||
|
||||
private createPooled(): PooledEngine {
|
||||
const engine = this.config.factory(this.config.engineConfig);
|
||||
return {
|
||||
engine,
|
||||
inUse: false,
|
||||
executionCount: 0,
|
||||
lastUsed: this.clock(),
|
||||
createdAt: this.clock(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { DEFAULTS as DEFAULT_POOL_CONFIG };
|
||||
Reference in New Issue
Block a user