{"record":{"id":"9743fe654fd4dfaf","repo":"ruvnet/ruflo","slug":"connection-pool-is-shutting-down-9743fe","errorCode":null,"errorMessage":"Connection pool is shutting down","messagePattern":"Connection pool is shutting down","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/shared/src/mcp/connection-pool.ts","lineNumber":169,"sourceCode":"    const connection = new ManagedConnection(id, this.transportType);\n\n    this.connections.set(id, connection);\n    this.stats.totalCreated++;\n\n    this.emit('pool:connection:created', { connectionId: id });\n    this.logger.debug('Connection created', { id, total: this.connections.size });\n\n    return connection;\n  }\n\n  /**\n   * Acquire a connection from the pool\n   */\n  async acquire(): Promise<PooledConnection> {\n    const startTime = performance.now();\n\n    if (this.isShuttingDown) {\n      throw new Error('Connection pool is shutting down');\n    }\n\n    // Try to find an idle connection\n    for (const connection of this.connections.values()) {\n      if (connection.state === 'idle' && connection.isHealthy()) {\n        connection.acquire();\n        this.stats.totalAcquired++;\n        this.recordAcquireTime(startTime);\n\n        this.emit('pool:connection:acquired', { connectionId: connection.id });\n        this.logger.debug('Connection acquired from pool', { id: connection.id });\n\n        return connection;\n      }\n    }\n\n    // Create new connection if under limit\n    if (this.connections.size < this.config.maxConnections) {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/shared/src/mcp/connection-pool.ts#L151-L187","documentation":"The MCP connection pool's acquire() refuses to hand out a connection once shutdown() has begun: during teardown idle connections are destroyed and the pool drains, so any handle granted mid-shutdown would be dead on arrival. The internal isShuttingDown flag is checked at the top of acquire(), making the race fail fast instead of returning a broken connection. Note isShuttingDown is private; pool.isHealthy() indirectly reflects it (it returns false when shutting down).","triggerScenarios":"An in-flight request calls acquire() after a SIGTERM handler invoked pool.shutdown(); a background loop (heartbeat, keepalive, worker poll) keeps acquiring while the app closes; the HTTP server still accepts new work after pool.shutdown() was called; a test's afterAll closes a shared pool while a dangling async test still acquires.","commonSituations":"Graceful-shutdown ordering bugs: closing the pool before the listener that feeds it; long-lived workers never signalled to stop before teardown; retry wrappers that treat this terminal error as transient and keep looping.","solutions":["Fix teardown order: stop admitting new work (close the HTTP server, set a draining flag), await outstanding requests, THEN call pool.shutdown()","Gate every acquire with a caller-side closing flag, and treat 'Connection pool is shutting down' as a terminal, non-retryable signal","Await pool.shutdown() fully during teardown so no acquire can race it","In tests, ensure every request has settled before closing a shared pool in afterAll"],"exampleFix":"// before\nprocess.on('SIGTERM', () => pool.shutdown());\napp.listen(3000); // requests still arrive; acquire() races shutdown\n\n// after\nprocess.on('SIGTERM', async () => {\n  draining = true;                  // gate new acquires in the request path\n  await closeServerGracefully();    // stop admitting work first\n  await pool.shutdown();            // now drain safely\n});","handlingStrategy":"try-catch","validationCode":"if (draining || !pool.isHealthy()) {\n  throw new ServiceUnavailableError('pool is draining');\n}\nconst conn = await pool.acquire(); // still wrap in try-catch: isHealthy() can race shutdown","typeGuard":null,"tryCatchPattern":"try {\n  return await withTimeout(pool.acquire(), 5_000);\n} catch (e) {\n  if (e instanceof Error && /shutting down/i.test(e.message)) {\n    throw new ServiceUnavailableError('MCP pool is shutting down'); // terminal: never retry\n  }\n  throw e; // other errors may be transient and handled by the caller\n}","preventionTips":["Order teardown: stop admitting work, await in-flight requests, then pool.shutdown()","Keep a caller-side draining flag checked before every acquire","Classify 'shutting down' as non-retryable so retry loops terminate instead of spinning","In tests, close shared pools only after all requests have settled"],"tags":["mcp","connection-pool","shutdown","race-condition","lifecycle"],"backgroundTag":"connection-pool-closed","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}