diff --git a/packages/pools/src/Pools/Pool.php b/packages/pools/src/Pools/Pool.php index d56d7cc2..f57c756e 100644 --- a/packages/pools/src/Pools/Pool.php +++ b/packages/pools/src/Pools/Pool.php @@ -376,19 +376,28 @@ public function pop(): Connection }); if ($shouldCreateConnections) { + $reserved = true; + try { $connection = $this->createConnection(); $this->pool->synchronized(function () use ($connection): void { $this->active[$connection->getID()] = $connection; }); + $reserved = false; + return $connection; - } catch (\Exception $e) { - $this->pool->synchronized(function (): void { - $this->connectionsCreated--; - }); + } catch (\Throwable $e) { + // Throwable, not Exception: the init callback is caller + // supplied, so an Error holds the slot just the same. // Don't throw immediately - fall through to try getting // an existing connection from the pool $lastException = $e; + } finally { + if ($reserved) { + $this->pool->synchronized(function (): void { + $this->connectionsCreated--; + }); + } } } diff --git a/packages/pools/tests/Pools/Scopes/PoolTestScope.php b/packages/pools/tests/Pools/Scopes/PoolTestScope.php index eb46b893..240ca2cd 100644 --- a/packages/pools/tests/Pools/Scopes/PoolTestScope.php +++ b/packages/pools/tests/Pools/Scopes/PoolTestScope.php @@ -341,6 +341,35 @@ public function testPopRetriesAfterConnectionCreationFailure(): void }); } + public function testPopReleasesReservedSlotWhenCreationThrowsError(): void + { + $this->execute(function (): void { + // pop() reserves capacity before the connection exists. An Error + // escaping the catch used to keep that slot, draining the pool one + // failed pop at a time. + $pool = new Pool($this->getAdapter(), 'test-error-leak', 2, function (): string { + throw new \TypeError('Connection init failed'); + }); + $pool->setReconnectAttempts(1); + $pool->setReconnectSleep(0); + $pool->setRetryAttempts(1); + $pool->setRetrySleep(0); + + // More attempts than slots, so a kept reservation shows up as lost + // capacity by the end. + for ($i = 0; $i < 5; $i++) { + try { + $pool->pop(); + $this->fail('Should have thrown'); + } catch (Exception $e) { + $this->assertInstanceOf(\TypeError::class, $e->getPrevious()); + } + } + + $this->assertSame(2, $pool->count()); + }); + } + public function testPoolEmptyErrorIncludesActiveCount(): void { $this->execute(function (): void {