Skip to content

Commit

Permalink
Register the idle connection listener
Browse files Browse the repository at this point in the history
  • Loading branch information
alli83 committed May 2, 2024
1 parent 5418e81 commit 6bca630
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 14 deletions.
6 changes: 6 additions & 0 deletions config/dbal.xml
Expand Up @@ -101,6 +101,12 @@
<tag name="controller.service_arguments" />
</service>

<service id="doctrine.dbal.idle_connection_listener" class="Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener">
<argument type="service" id="doctrine.dbal.connection_expiries" />
<argument type="service" id="service_container" />
<tag name="kernel.event_subscriber" />
</service>

<service id="doctrine.dbal.default_schema_manager_factory" class="Doctrine\DBAL\Schema\DefaultSchemaManagerFactory" />
<service id="doctrine.dbal.legacy_schema_manager_factory" class="Doctrine\DBAL\Schema\LegacySchemaManagerFactory" />

Expand Down
5 changes: 5 additions & 0 deletions config/middlewares.xml
Expand Up @@ -5,6 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="doctrine.dbal.connection_expiries" class="ArrayObject" />
<service id="doctrine.dbal.logging_middleware" class="Doctrine\DBAL\Logging\Middleware" abstract="true">
<argument type="service" id="logger" />
<tag name="monolog.logger" channel="doctrine" />
Expand All @@ -17,5 +18,9 @@
<argument type="service" id="doctrine.debug_data_holder" />
<argument type="service" id="debug.stopwatch" on-invalid="null" />
</service>
<service id="doctrine.dbal.idle_connection_middleware" class="Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware" abstract="true">
<argument type="service" id="doctrine.dbal.connection_expiries" />
<argument /> <!-- check timing -->
</service>
</services>
</container>
2 changes: 2 additions & 0 deletions psalm.xml.dist
Expand Up @@ -43,6 +43,8 @@
<referencedClass name="Doctrine\ORM\ORMException"/>
<!-- Dropped in DBAL 4 -->
<referencedClass name="Doctrine\DBAL\Exception"/>
<!-- Available starting from Symfony 7.1 -->
<referencedClass name="Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver"/>
</errorLevel>
</UndefinedClass>
<DuplicateClass>
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Expand Up @@ -221,6 +221,7 @@ private function getDbalConnectionsNode(): ArrayNodeDefinition
->end()
->booleanNode('disable_type_comments')->end()
->scalarNode('server_version')->end()
->integerNode('idle_connection_ttl')->defaultValue(600)->end()
->scalarNode('driver_class')->end()
->scalarNode('wrapper_class')->end()
->booleanNode('keep_slave')
Expand Down
30 changes: 27 additions & 3 deletions src/DependencyInjection/DoctrineExtension.php
Expand Up @@ -37,6 +37,7 @@
use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension;
use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor;
use Symfony\Bridge\Doctrine\SchemaListener\DoctrineDbalCacheAdapterSchemaListener;
use Symfony\Bridge\Doctrine\SchemaListener\LockStoreSchemaListener;
Expand Down Expand Up @@ -83,7 +84,7 @@
*
* @final since 2.9
* @psalm-type DBALConfig = array{
* connections: array<string, array{logging: bool, profiling: bool, profiling_collect_backtrace: bool}>,
* connections: array<string, array{logging: bool, profiling: bool, profiling_collect_backtrace: bool, idle_connection_ttl: int}>,
* driver_schemes: array<string, string>,
* default_connection: string,
* types: array<string, string>,
Expand Down Expand Up @@ -196,6 +197,8 @@ protected function dbalLoad(array $config, ContainerBuilder $container)
$connWithLogging = [];
$connWithProfiling = [];
$connWithBacktrace = [];
$ttlByConnection = [];

foreach ($config['connections'] as $name => $connection) {
if ($connection['logging']) {
$connWithLogging[] = $name;
Expand All @@ -209,6 +212,10 @@ protected function dbalLoad(array $config, ContainerBuilder $container)
}
}

if ($connection['idle_connection_ttl'] > 0) {
$ttlByConnection[$name] = $connection['idle_connection_ttl'];
}

$this->loadDbalConnection($name, $connection, $container);
}

Expand All @@ -228,7 +235,16 @@ protected function dbalLoad(array $config, ContainerBuilder $container)
}
});

$this->registerDbalMiddlewares($container, $connWithLogging, $connWithProfiling, $connWithBacktrace);
$this->registerDbalMiddlewares($container, $connWithLogging, $connWithProfiling, $connWithBacktrace, array_keys($ttlByConnection));

$container->getDefinition('doctrine.dbal.idle_connection_middleware')->setArgument(1, $ttlByConnection);

if (class_exists(Listener::class)) {
return;
}

$container->removeDefinition('doctrine.dbal.idle_connection_listener');
$container->removeDefinition('doctrine.dbal.idle_connection_middleware');
}

/**
Expand Down Expand Up @@ -1186,12 +1202,14 @@ private function createArrayAdapterCachePool(ContainerBuilder $container, string
* @param string[] $connWithLogging
* @param string[] $connWithProfiling
* @param string[] $connWithBacktrace
* @param string[] $connWithTtl
*/
private function registerDbalMiddlewares(
ContainerBuilder $container,
array $connWithLogging,
array $connWithProfiling,
array $connWithBacktrace
array $connWithBacktrace,
array $connWithTtl
): void {
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
$loader->load('middlewares.xml');
Expand All @@ -1207,5 +1225,11 @@ private function registerDbalMiddlewares(
$debugMiddlewareAbstractDef
->addTag('doctrine.middleware', ['connection' => $connName, 'priority' => 10]);
}

$idleConnectionMiddlewareAbstractDef = $container->getDefinition('doctrine.dbal.idle_connection_middleware');
foreach ($connWithTtl as $connName) {
$idleConnectionMiddlewareAbstractDef
->addTag('doctrine.middleware', ['connection' => $connName, 'priority' => 10]);
}
}
}
36 changes: 36 additions & 0 deletions src/Middleware/IdleConnectionMiddleware.php
@@ -0,0 +1,36 @@
<?php

namespace Doctrine\Bundle\DoctrineBundle\Middleware;

use ArrayObject;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver as IdleConnectionDriver;

class IdleConnectionMiddleware implements Middleware, ConnectionNameAwareInterface
{
private ArrayObject $connectionExpiries;
/** @var array<string, int> */
private array $ttlByConnection;
private string $connectionName;

/**
* @param ArrayObject<string, int> $connectionExpiries
* @param array<string, int> $ttlByConnection
*/
public function __construct(ArrayObject $connectionExpiries, array $ttlByConnection)
{
$this->connectionExpiries = $connectionExpiries;
$this->ttlByConnection = $ttlByConnection;
}

public function setConnectionName(string $name): void
{
$this->connectionName = $name;
}

public function wrap(Driver $driver): Driver

Check failure on line 32 in src/Middleware/IdleConnectionMiddleware.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm

InvalidReturnType

src/Middleware/IdleConnectionMiddleware.php:32:43: InvalidReturnType: The declared return type 'Doctrine\DBAL\Driver' for Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware::wrap is incorrect, got 'Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver' (see https://psalm.dev/011)
{
return new IdleConnectionDriver($driver, $this->connectionExpiries, $this->ttlByConnection[$this->connectionName], $this->connectionName);

Check failure on line 34 in src/Middleware/IdleConnectionMiddleware.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm

InvalidReturnStatement

src/Middleware/IdleConnectionMiddleware.php:34:16: InvalidReturnStatement: The inferred type 'Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver' does not match the declared return type 'Doctrine\DBAL\Driver' for Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware::wrap (see https://psalm.dev/128)
}
}
4 changes: 4 additions & 0 deletions tests/DependencyInjection/AbstractDoctrineExtensionTest.php
Expand Up @@ -220,6 +220,7 @@ public function testDbalLoadSinglePrimaryReplicaConnection(): void
'host' => 'localhost',
'unix_socket' => '/path/to/mysqld.sock',
'driverOptions' => [PDO::ATTR_STRINGIFY_FETCHES => 1],
'idle_connection_ttl' => 600,
],
$param['primary'],
);
Expand Down Expand Up @@ -340,6 +341,7 @@ public function testLoadSimpleSingleConnection(): void
'driver' => 'pdo_mysql',
'driverOptions' => [],
'defaultTableOptions' => [],
'idle_connection_ttl' => 600,
],
new Reference('doctrine.dbal.default_connection.configuration'),
method_exists(Connection::class, 'getEventManager')
Expand Down Expand Up @@ -379,6 +381,7 @@ public function testLoadSimpleSingleConnectionWithoutDbName(): void
'driver' => 'pdo_mysql',
'driverOptions' => [],
'defaultTableOptions' => [],
'idle_connection_ttl' => 600,
],
new Reference('doctrine.dbal.default_connection.configuration'),
method_exists(Connection::class, 'getEventManager')
Expand Down Expand Up @@ -418,6 +421,7 @@ public function testLoadSingleConnection(): void
'dbname' => 'sqlite_db',
'memory' => true,
'defaultTableOptions' => [],
'idle_connection_ttl' => 600,
],
new Reference('doctrine.dbal.default_connection.configuration'),
method_exists(Connection::class, 'getEventManager')
Expand Down
45 changes: 34 additions & 11 deletions tests/DependencyInjection/Compiler/MiddlewarePassTest.php
Expand Up @@ -6,15 +6,18 @@
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\MiddlewaresPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension;
use Doctrine\Bundle\DoctrineBundle\Middleware\ConnectionNameAwareInterface;
use Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Bridge\Doctrine\Middleware\IdleConnection\Listener;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;

use function array_map;
use function class_exists;
use function implode;
use function sprintf;

Expand Down Expand Up @@ -170,7 +173,8 @@ public function testAddMiddlewareOrderingWithDefaultPriority(): void

$this->assertMiddlewareInjected($container, 'conn1', PHP7Middleware::class);
$this->assertMiddlewareInjected($container, 'conn1', ConnectionAwarePHP7Middleware::class, true);
$this->assertMiddlewareOrdering($container, 'conn1', [PHP7Middleware::class, ConnectionAwarePHP7Middleware::class]);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, PHP7Middleware::class, ConnectionAwarePHP7Middleware::class] : [PHP7Middleware::class, ConnectionAwarePHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
}

public function testAddMiddlewareOrderingWithExplicitPriority(): void
Expand All @@ -193,7 +197,8 @@ public function testAddMiddlewareOrderingWithExplicitPriority(): void

$this->assertMiddlewareInjected($container, 'conn1', PHP7Middleware::class);
$this->assertMiddlewareInjected($container, 'conn1', ConnectionAwarePHP7Middleware::class, true);
$this->assertMiddlewareOrdering($container, 'conn1', [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class]);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, ConnectionAwarePHP7Middleware::class, PHP7Middleware::class] : [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
}

public function testAddMiddlewareOrderingWithExplicitPriorityAndConnection(): void
Expand Down Expand Up @@ -222,7 +227,8 @@ public function testAddMiddlewareOrderingWithExplicitPriorityAndConnection(): vo
$this->assertMiddlewareInjected($container, 'conn1', ConnectionAwarePHP7Middleware::class, true);
$this->assertMiddlewareInjected($container, 'conn2', PHP7Middleware::class);
$this->assertMiddlewareNotInjected($container, 'conn2', ConnectionAwarePHP7Middleware::class);
$this->assertMiddlewareOrdering($container, 'conn1', [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class]);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, ConnectionAwarePHP7Middleware::class, PHP7Middleware::class] : [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
}

public function testAddMiddlewareOrderingWithExplicitPriorityPerConnection(): void
Expand Down Expand Up @@ -252,8 +258,10 @@ public function testAddMiddlewareOrderingWithExplicitPriorityPerConnection(): vo
$this->assertMiddlewareInjected($container, 'conn1', ConnectionAwarePHP7Middleware::class, true);
$this->assertMiddlewareInjected($container, 'conn2', PHP7Middleware::class);
$this->assertMiddlewareInjected($container, 'conn2', ConnectionAwarePHP7Middleware::class, true);
$this->assertMiddlewareOrdering($container, 'conn1', [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class]);
$this->assertMiddlewareOrdering($container, 'conn2', [PHP7Middleware::class, ConnectionAwarePHP7Middleware::class]);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, ConnectionAwarePHP7Middleware::class, PHP7Middleware::class] : [ConnectionAwarePHP7Middleware::class, PHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, PHP7Middleware::class, ConnectionAwarePHP7Middleware::class] : [PHP7Middleware::class, ConnectionAwarePHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn2', $expectedMiddlewares);
}

public function testAddMiddlewareOrderingWithInheritedPriorityPerConnection(): void
Expand Down Expand Up @@ -292,8 +300,10 @@ public function testAddMiddlewareOrderingWithInheritedPriorityPerConnection(): v
$this->assertMiddlewareInjected($container, 'conn2', PHP7Middleware::class);
$this->assertMiddlewareNotInjected($container, 'conn2', ConnectionAwarePHP7Middleware::class);
$this->assertMiddlewareInjected($container, 'conn2', 'some_middleware_class');
$this->assertMiddlewareOrdering($container, 'conn1', [ConnectionAwarePHP7Middleware::class, 'some_middleware_class', PHP7Middleware::class]);
$this->assertMiddlewareOrdering($container, 'conn2', [PHP7Middleware::class, 'some_middleware_class']);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, ConnectionAwarePHP7Middleware::class, 'some_middleware_class', PHP7Middleware::class] : [ConnectionAwarePHP7Middleware::class, 'some_middleware_class', PHP7Middleware::class];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
$expectedMiddlewares = class_exists(Listener::class) ? [IdleConnectionMiddleware::class, PHP7Middleware::class, 'some_middleware_class'] : [PHP7Middleware::class, 'some_middleware_class'];
$this->assertMiddlewareOrdering($container, 'conn2', $expectedMiddlewares);
}

/** @requires PHP 8 */
Expand Down Expand Up @@ -327,15 +337,28 @@ public function testAddMiddlewareOrderingWithAttributeForAutoconfiguration(): vo
$this->assertMiddlewareInjected($container, 'conn2', AutoconfiguredMiddleware::class);
$this->assertMiddlewareInjected($container, 'conn2', AutoconfiguredMiddlewareWithConnection::class);
$this->assertMiddlewareInjected($container, 'conn2', AutoconfiguredMiddlewareWithPriority::class);
$this->assertMiddlewareOrdering($container, 'conn1', [
$expectedMiddlewares = class_exists(Listener::class) ? [
IdleConnectionMiddleware::class,
AutoconfiguredMiddlewareWithPriority::class,
AutoconfiguredMiddleware::class,
]);
$this->assertMiddlewareOrdering($container, 'conn2', [
] :
[
AutoconfiguredMiddlewareWithPriority::class,
AutoconfiguredMiddleware::class,
];
$this->assertMiddlewareOrdering($container, 'conn1', $expectedMiddlewares);
$expectedMiddlewares = class_exists(Listener::class) ? [
IdleConnectionMiddleware::class,
AutoconfiguredMiddlewareWithPriority::class,
AutoconfiguredMiddleware::class,
AutoconfiguredMiddlewareWithConnection::class,
]);
] :
[
AutoconfiguredMiddlewareWithPriority::class,
AutoconfiguredMiddleware::class,
AutoconfiguredMiddlewareWithConnection::class,
];
$this->assertMiddlewareOrdering($container, 'conn2', $expectedMiddlewares);
}

private function createContainer(callable $func, bool $addConnections = true): ContainerBuilder
Expand Down
51 changes: 51 additions & 0 deletions tests/DependencyInjection/DoctrineExtensionTest.php
Expand Up @@ -1424,6 +1424,57 @@ public function testDefinitionsToLogQueriesLoggingFalse(): void
$this->assertArrayNotHasKey('doctrine.middleware', $abstractMiddlewareDefTags);
}

/** @requires function Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver::__construct */
public function testDefinitionsIdleConnection(): void
{
$container = $this->getContainer();
$extension = new DoctrineExtension();

$config = BundleConfigurationBuilder::createBuilder()
->addConnection([
'connections' => [
'conn1' => [
'password' => 'foo',
'logging' => false,
'profiling' => false,
'idle_connection_ttl' => 15,
],
'conn2' => [
'password' => 'bar',
'logging' => false,
'profiling' => true,
],
],
])
->build();

$extension->load([$config], $container);

$this->assertTrue($container->hasDefinition('doctrine.dbal.idle_connection_middleware'));

$abstractMiddlewareDef = $container->getDefinition('doctrine.dbal.idle_connection_middleware');
$ttlByConnection = $abstractMiddlewareDef->getArgument(1);

$this->assertArrayHasKey('conn1', $ttlByConnection);
$this->assertEquals(15, $ttlByConnection['conn1']);
$this->assertArrayHasKey('conn2', $ttlByConnection);
$this->assertEquals(600, $ttlByConnection['conn2']);

$abstractMiddlewareDefTags = $container->getDefinition('doctrine.dbal.idle_connection_middleware')->getTags();

$idleConnectionMiddlewareTagAttributes = [];
foreach ($abstractMiddlewareDefTags as $tag => $attributes) {
if ($tag !== 'doctrine.middleware') {
continue;
}

$idleConnectionMiddlewareTagAttributes = $attributes;
}

$this->assertTrue(in_array(['connection' => 'conn1', 'priority' => 10], $idleConnectionMiddlewareTagAttributes, true), 'Tag with connection conn1 not found for doctrine.dbal.idle_connection_middleware');
$this->assertTrue(in_array(['connection' => 'conn2', 'priority' => 10], $idleConnectionMiddlewareTagAttributes, true), 'Tag with connection conn2 found for doctrine.dbal.idle_connection_middleware');
}

/**
* @requires function \Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver::__construct
* @testWith [true]
Expand Down

0 comments on commit 6bca630

Please sign in to comment.