{"record":{"id":"58c889386d0c4096","repo":"pydantic/monty","slug":"pool-is-closed","errorCode":null,"errorMessage":"pool is closed","messagePattern":"pool is closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/monty-js/ts/worker/pool.ts","lineNumber":112,"sourceCode":"  private constructor(\n    private readonly factory: WorkerFactory,\n    private readonly maxWorkers: number,\n    private readonly maxCheckouts: number | undefined,\n  ) {}\n\n  /** Creates the pool and prewarms `minWorkers` idle workers. */\n  static async create(factory: WorkerFactory, options: WorkerPoolOptions = {}): Promise<WorkerPool> {\n    const max = Math.max(1, options.maxWorkers ?? 4)\n    const min = Math.min(Math.max(0, options.minWorkers ?? 1), max)\n    const pool = new WorkerPool(factory, max, options.maxCheckoutsPerWorker)\n    const warm = await Promise.all(Array.from({ length: min }, () => pool.spawn()))\n    pool.idle.push(...warm)\n    return pool\n  }\n\n  /** Borrows a worker and returns a session bound to it. */\n  async checkout(config: WorkerSessionConfig = {}): Promise<MontySession> {\n    if (this.closed) throw new Error('pool is closed')\n    const slot = await this.acquire()\n    let transport: WorkerTransport\n    try {\n      transport = await WorkerTransport.create(slot.worker.dispatch, config)\n    } catch (err) {\n      this.discard(slot)\n      throw err\n    }\n    transport.onFinish = (reusable) => this.release(slot, reusable)\n    return new MontySession(transport as unknown as SessionNative)\n  }\n\n  /** Terminates every worker and rejects anyone still waiting. */\n  async close(): Promise<void> {\n    this.closed = true\n    for (const waiter of this.waiters.splice(0)) waiter.reject(new Error('pool is closed'))\n    for (const slot of this.idle.splice(0)) {\n      slot.worker.terminate()","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty-js/ts/worker/pool.ts#L94-L130","documentation":"`WorkerPool.checkout()` refuses to borrow a worker once the pool has been closed, because closing shuts down worker dispatch and no new session can be served. This is a fail-fast guard so you get a clear synchronous error rather than hangs or crashes inside `WorkerTransport.create`.","triggerScenarios":"Calling `pool.checkout(config)` after `await pool.close()` (or after `await using` disposal of the pool) has completed.","commonSituations":"Reusing a pool object captured in a longer-lived variable after an `await using` scope ends; closing a pool in a shutdown handler while background tasks still hold a reference; accidentally closing in a test fixture teardown that runs before remaining assertions.","solutions":["Create a fresh pool with `createWorkerPool(modules)` (or `Monty.create()`) and call `checkout()` on that instead of the closed one.","Move `checkout()` calls inside the lifetime of the pool (`await using pool = ...; ... checkout within that scope`).","Track pool lifecycle in application code: set a flag or null out the reference when `close()` is awaited so late callers can be rerouted to a new pool.","In long-running services, keep one pool per application lifetime and never close it per-request."],"exampleFix":"// before\nawait using pool = await createWorkerPool(modules)\nawait pool.close()\nconst session = await pool.checkout() // throws\n\n// after\nawait using pool = await createWorkerPool(modules)\nconst session = await pool.checkout()\nawait session.feedRun(code)\n// pool closes automatically at scope end","handlingStrategy":"validation","validationCode":"function ensureOpen(pool: { closed: boolean }) {\n  if (pool.closed) throw new Error('attempt to checkout a closed pool')\n}","typeGuard":"function isUsable(pool: WorkerPool | null | undefined): pool is WorkerPool {\n  return pool != null && !pool.closed\n}","tryCatchPattern":"try {\n  session = await pool.checkout(config)\n} catch (err) {\n  if (err instanceof Error && err.message === 'pool is closed') {\n    pool = await createWorkerPool(modules)\n    session = await pool.checkout(config)\n  } else throw err\n}","preventionTips":["Use `await using` so pool lifetime scopes all checkouts.","Never store the pool in globals that outlive close().","Null out the pool reference after close() resolves.","Create one long-lived pool per app instead of closing per operation."],"tags":["pool","lifecycle","wasm-worker","resource-management"],"backgroundTag":"invalid-state-transition","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}