2.8 KiB
SandboxPool
(no submodules)
Purpose
SandboxPool amortises engine startup cost across many short executions by maintaining a warm pool of SandboxEngine instances. The pool pre-warms to minSize on construction, lazily grows up to maxSize under load, recycles engines that exceed maxExecutionsPerEngine (so leak-prone engines like long-lived workers don't accumulate state), and reclaims idle engines above minSize whose lastUsed exceeds idleTimeoutMs via the explicit cleanupTick() call.
The pool's clock is injectable — defaulting to Date.now in production, replaced by a FakeClock in tests so the idle-reclaim behaviour can be asserted without flake. The cleanup is invoked explicitly rather than on an internal timer so the consumer controls scheduling and the pool stays single-threaded.
Features
- Pre-warmed up to
minSizeon construction. - Lazy growth up to
maxSizeon acquisition. POOL_EXHAUSTEDsurfaced in aSandboxResultwhen the pool cannot grow.- Per-engine execution count, recycle on threshold.
cleanupTick()reclaims idle engines down tominSize.- Injectable
Clockfor deterministic tests. - OTel
recordPoolAcquisitionfor each acquisition (acquired / created / exhausted). - Aggregate
stats()surface.
Usage: Basic
import { VmEngine } from "@suaveplan/sandbox/vm-engine";
import { SandboxPool } from "@suaveplan/sandbox/pool";
const pool = new SandboxPool({
minSize: 2,
maxSize: 8,
engineConfig: { timeout: 5000, maxOutputSize: 1024, memoryLimit: 5e7 },
factory: (cfg) => new VmEngine(cfg),
});
const r = await pool.execute("1 + 1");
pool.destroy();
Usage: Advanced
Drive cleanup against a FakeClock so idle reclamation can be asserted:
let now = 0;
const pool = new SandboxPool({
engineConfig,
factory,
minSize: 1,
maxSize: 4,
idleTimeoutMs: 1000,
clock: () => now,
});
// run a few executions, then advance time:
now = 5000;
pool.cleanupTick();
API Reference
new SandboxPool(config).execute(code, context?, options?): Promise<SandboxResult>.size(),available(),inUse(),stats().warmUp(),cleanupTick(),destroy().DEFAULT_POOL_CONFIG— exported defaults for downstream composition.
Implementation Notes
acquire prefers idle engines over creating new ones — this keeps warmed
engines reusable rather than stranding them behind a fresh allocation when an
old engine is right there. Recycling is reactive (on release, after the
threshold is crossed) rather than proactive (at acquisition time) so a
recycle never delays the path that needs the engine. The clock lookup
happens once per lastUsed/createdAt write so callers can swap the clock
implementation without re-engineering the pool's call graph.