Skip to content

Commit bfd37f7

Browse files
alli83nicolas-grekas
authored andcommitted
[DoctrineBridge] Idle connection listener for long running runtime
1 parent 1deb162 commit bfd37f7

File tree

6 files changed

+184
-1
lines changed

6 files changed

+184
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

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

1011
7.0
1112
---

Middleware/IdleConnection/Driver.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;
13+
14+
use Doctrine\DBAL\Driver as DriverInterface;
15+
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
16+
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
17+
18+
final class Driver extends AbstractDriverMiddleware
19+
{
20+
public function __construct(
21+
DriverInterface $driver,
22+
private \ArrayObject $connectionExpiries,
23+
private readonly int $ttl,
24+
private readonly string $connectionName,
25+
) {
26+
parent::__construct($driver);
27+
}
28+
29+
public function connect(array $params): ConnectionInterface
30+
{
31+
$timestamp = time();
32+
$connection = parent::connect($params);
33+
$this->connectionExpiries[$this->connectionName] = $timestamp + $this->ttl;
34+
35+
return $connection;
36+
}
37+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Middleware\IdleConnection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpKernel\Event\RequestEvent;
17+
use Symfony\Component\HttpKernel\KernelEvents;
18+
19+
final class Listener implements EventSubscriberInterface
20+
{
21+
/**
22+
* @param \ArrayObject<string, int> $connectionExpiries
23+
*/
24+
public function __construct(
25+
private readonly \ArrayObject $connectionExpiries,
26+
private ContainerInterface $container,
27+
) {
28+
}
29+
30+
public function onKernelRequest(RequestEvent $event): void
31+
{
32+
$timestamp = time();
33+
34+
foreach ($this->connectionExpiries as $name => $expiry) {
35+
if ($timestamp >= $expiry) {
36+
// unset before so that we won't retry in case of any failure
37+
$this->connectionExpiries->offsetUnset($name);
38+
39+
try {
40+
$connection = $this->container->get("doctrine.dbal.{$name}_connection");
41+
$connection->close();
42+
} catch (\Exception) {
43+
// ignore exceptions to remain fail-safe
44+
}
45+
}
46+
}
47+
}
48+
49+
public static function getSubscribedEvents(): array
50+
{
51+
return [
52+
KernelEvents::REQUEST => 'onKernelRequest',
53+
];
54+
}
55+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Tests\Middleware\IdleConnection;
13+
14+
use Doctrine\DBAL\Driver as DriverInterface;
15+
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
16+
use PHPUnit\Framework\TestCase;
17+
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver;
18+
19+
class DriverTest extends TestCase
20+
{
21+
/**
22+
* @group time-sensitive
23+
*/
24+
public function testConnect()
25+
{
26+
$driverMock = $this->createMock(DriverInterface::class);
27+
$connectionMock = $this->createMock(ConnectionInterface::class);
28+
29+
$driverMock->expects($this->once())
30+
->method('connect')
31+
->willReturn($connectionMock);
32+
33+
$connectionExpiries = new \ArrayObject();
34+
35+
$driver = new Driver($driverMock, $connectionExpiries, 60, 'default');
36+
$connection = $driver->connect([]);
37+
38+
$this->assertSame($connectionMock, $connection);
39+
$this->assertArrayHasKey('default', $connectionExpiries);
40+
$this->assertSame(time() + 60, $connectionExpiries['default']);
41+
}
42+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Middleware\IdleConnection;
13+
14+
use Doctrine\DBAL\Connection as ConnectionInterface;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
use Symfony\Component\HttpKernel\Event\RequestEvent;
19+
20+
class ListenerTest extends TestCase
21+
{
22+
public function testOnKernelRequest()
23+
{
24+
$containerMock = $this->createMock(ContainerInterface::class);
25+
$connectionExpiries = new \ArrayObject(['connectionone' => time() - 30, 'connectiontwo' => time() + 40]);
26+
27+
$connectionOneMock = $this->getMockBuilder(ConnectionInterface::class)
28+
->disableOriginalConstructor()
29+
->getMock();
30+
31+
$containerMock->expects($this->exactly(1))
32+
->method('get')
33+
->with('doctrine.dbal.connectionone_connection')
34+
->willReturn($connectionOneMock);
35+
36+
$listener = new Listener($connectionExpiries, $containerMock);
37+
38+
$listener->onKernelRequest($this->createMock(RequestEvent::class));
39+
40+
$this->assertArrayNotHasKey('connectionone', (array) $connectionExpiries);
41+
$this->assertArrayHasKey('connectiontwo', (array) $connectionExpiries);
42+
}
43+
}

phpunit.xml.dist

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@
3333
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
3434
<arguments>
3535
<array>
36-
<element key="time-sensitive"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
36+
<element key="time-sensitive">
37+
<array>
38+
<element key="0"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
39+
<element key="1"><string>Symfony\Bridge\Doctrine\Middleware\Debug</string></element>
40+
</array>
41+
</element>
3742
</array>
3843
</arguments>
3944
</listener>

0 commit comments

Comments
 (0)