Skip to content

Commit

Permalink
feature #53214 [DoctrineBridge] Idle connection listener for long run…
Browse files Browse the repository at this point in the history
…ning runtime (alli83)

This PR was merged into the 7.1 branch.

Discussion
----------

[DoctrineBridge] Idle connection listener for long running runtime

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | Fix #51661
| License       | MIT

This pull request introduces a solution based on the RoadRunner bundle's Doctrine ORM/ODM middleware https://github.com/Baldinof/roadrunner-bundle/blob/3.x/src/Integration/Doctrine/DoctrineORMMiddleware.php#L22.
It checks the status of Doctrine connection, then if the connection is initialized and connected, it performs a 'ping' to check its viability. If the ping fails, it closes the connection.

linked to doctrine/DoctrineBundle#1739

Commits
-------

f7cc44e [DoctrineBridge] Idle connection listener for long running runtime
  • Loading branch information
nicolas-grekas committed Apr 25, 2024
2 parents 0d7c20a + f7cc44e commit 462ab95
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 8 deletions.
15 changes: 8 additions & 7 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@
<array>
<element key="0"><string>Cache\IntegrationTests</string></element>
<element key="1"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
<element key="2"><string>Symfony\Component\Cache</string></element>
<element key="3"><string>Symfony\Component\Cache\Tests\Fixtures</string></element>
<element key="4"><string>Symfony\Component\Cache\Tests\Traits</string></element>
<element key="5"><string>Symfony\Component\Cache\Traits</string></element>
<element key="6"><string>Symfony\Component\Console</string></element>
<element key="7"><string>Symfony\Component\HttpFoundation</string></element>
<element key="8"><string>Symfony\Component\Uid</string></element>
<element key="2"><string>Symfony\Bridge\Doctrine\Middleware\IdleConnection</string></element>
<element key="3"><string>Symfony\Component\Cache</string></element>
<element key="4"><string>Symfony\Component\Cache\Tests\Fixtures</string></element>
<element key="5"><string>Symfony\Component\Cache\Tests\Traits</string></element>
<element key="6"><string>Symfony\Component\Cache\Traits</string></element>
<element key="7"><string>Symfony\Component\Console</string></element>
<element key="8"><string>Symfony\Component\HttpFoundation</string></element>
<element key="9"><string>Symfony\Component\Uid</string></element>
</array>
</element>
</array>
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bridge/Doctrine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Deprecate the `DoctrineExtractor::getTypes()` method, use `DoctrineExtractor::getType()` instead
* Allow `EntityValueResolver` to return a list of entities
* Add support for auto-closing idle connections

7.0
---
Expand Down
37 changes: 37 additions & 0 deletions src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Driver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;

use Doctrine\DBAL\Driver as DriverInterface;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;

final class Driver extends AbstractDriverMiddleware
{
public function __construct(
DriverInterface $driver,
private \ArrayObject $connectionExpiries,
private readonly int $ttl,
private readonly string $connectionName,
) {
parent::__construct($driver);
}

public function connect(array $params): ConnectionInterface
{
$timestamp = time();
$connection = parent::connect($params);
$this->connectionExpiries[$this->connectionName] = $timestamp + $this->ttl;

return $connection;
}
}
55 changes: 55 additions & 0 deletions src/Symfony/Bridge/Doctrine/Middleware/IdleConnection/Listener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class Listener implements EventSubscriberInterface
{
/**
* @param \ArrayObject<string, int> $connectionExpiries
*/
public function __construct(
private readonly \ArrayObject $connectionExpiries,
private ContainerInterface $container,
) {
}

public function onKernelRequest(RequestEvent $event): void
{
$timestamp = time();

foreach ($this->connectionExpiries as $name => $expiry) {
if ($timestamp >= $expiry) {
// unset before so that we won't retry in case of any failure
$this->connectionExpiries->offsetUnset($name);

try {
$connection = $this->container->get("doctrine.dbal.{$name}_connection");
$connection->close();
} catch (\Exception) {
// ignore exceptions to remain fail-safe
}
}
}
}

public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Doctrine\Tests\Middleware\IdleConnection;

use Doctrine\DBAL\Driver as DriverInterface;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver;

class DriverTest extends TestCase
{
/**
* @group time-sensitive
*/
public function testConnect()
{
$driverMock = $this->createMock(DriverInterface::class);
$connectionMock = $this->createMock(ConnectionInterface::class);

$driverMock->expects($this->once())
->method('connect')
->willReturn($connectionMock);

$connectionExpiries = new \ArrayObject();

$driver = new Driver($driverMock, $connectionExpiries, 60, 'default');
$connection = $driver->connect([]);

$this->assertSame($connectionMock, $connection);
$this->assertArrayHasKey('default', $connectionExpiries);
$this->assertSame(time() + 60, $connectionExpiries['default']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Middleware\IdleConnection;

use Doctrine\DBAL\Connection as ConnectionInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class ListenerTest extends TestCase
{
public function testOnKernelRequest()
{
$containerMock = $this->createMock(ContainerInterface::class);
$connectionExpiries = new \ArrayObject(['connectionone' => time() - 30, 'connectiontwo' => time() + 40]);

$connectionOneMock = $this->getMockBuilder(ConnectionInterface::class)
->disableOriginalConstructor()
->getMock();

$containerMock->expects($this->exactly(1))
->method('get')
->with('doctrine.dbal.connectionone_connection')
->willReturn($connectionOneMock);

$listener = new Listener($connectionExpiries, $containerMock);

$listener->onKernelRequest($this->createMock(RequestEvent::class));

$this->assertArrayNotHasKey('connectionone', (array) $connectionExpiries);
$this->assertArrayHasKey('connectiontwo', (array) $connectionExpiries);
}
}
7 changes: 6 additions & 1 deletion src/Symfony/Bridge/Doctrine/phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
<arguments>
<array>
<element key="time-sensitive"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
<element key="time-sensitive">
<array>
<element key="0"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
<element key="1"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
</array>
</element>
</array>
</arguments>
</listener>
Expand Down

0 comments on commit 462ab95

Please sign in to comment.