From a3c1c3f1ad0bef7894cebc16f0268d42c172a1f8 Mon Sep 17 00:00:00 2001 From: Allison Guilhem Date: Tue, 26 Dec 2023 22:55:13 +1100 Subject: [PATCH] Register the idle connection listener --- config/dbal.xml | 6 +++ config/middlewares.xml | 5 ++ psalm.xml.dist | 1 + src/DependencyInjection/Configuration.php | 1 + src/DependencyInjection/DoctrineExtension.php | 32 ++++++++++-- src/Middleware/IdleConnectionMiddleware.php | 37 ++++++++++++++ .../AbstractDoctrineExtensionTest.php | 4 ++ .../Compiler/MiddlewarePassTest.php | 45 ++++++++++++---- .../DoctrineExtensionTest.php | 51 +++++++++++++++++++ .../IdleConnectionMiddlewareTest.php | 29 +++++++++++ 10 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 src/Middleware/IdleConnectionMiddleware.php create mode 100644 tests/Middleware/IdleConnectionMiddlewareTest.php diff --git a/config/dbal.xml b/config/dbal.xml index a38d0de9e..61fcb5edf 100644 --- a/config/dbal.xml +++ b/config/dbal.xml @@ -101,6 +101,12 @@ + + + + + + diff --git a/config/middlewares.xml b/config/middlewares.xml index d6bf92bcc..00dad9c45 100644 --- a/config/middlewares.xml +++ b/config/middlewares.xml @@ -5,6 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + @@ -17,5 +18,9 @@ + + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 9a319d213..11489666d 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -43,6 +43,7 @@ + diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index ba0fed1d7..65c680dfd 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -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') diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index 161626a7a..d61ebc832 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -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; @@ -83,7 +84,7 @@ * * @final since 2.9 * @psalm-type DBALConfig = array{ - * connections: array, + * connections: array, * driver_schemes: array, * default_connection: string, * types: array, @@ -196,6 +197,9 @@ protected function dbalLoad(array $config, ContainerBuilder $container) $connWithLogging = []; $connWithProfiling = []; $connWithBacktrace = []; + $ttlByConnection = []; + $connWithTtl = []; + foreach ($config['connections'] as $name => $connection) { if ($connection['logging']) { $connWithLogging[] = $name; @@ -209,6 +213,11 @@ protected function dbalLoad(array $config, ContainerBuilder $container) } } + if ($connection['idle_connection_ttl'] > 0) { + $connWithTtl[] = $name; + $ttlByConnection[$name] = $connection['idle_connection_ttl']; + } + $this->loadDbalConnection($name, $connection, $container); } @@ -228,7 +237,16 @@ protected function dbalLoad(array $config, ContainerBuilder $container) } }); - $this->registerDbalMiddlewares($container, $connWithLogging, $connWithProfiling, $connWithBacktrace); + $this->registerDbalMiddlewares($container, $connWithLogging, $connWithProfiling, $connWithBacktrace, $connWithTtl); + + $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'); } /** @@ -1186,12 +1204,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'); @@ -1207,5 +1227,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]); + } } } diff --git a/src/Middleware/IdleConnectionMiddleware.php b/src/Middleware/IdleConnectionMiddleware.php new file mode 100644 index 000000000..401b5e2b7 --- /dev/null +++ b/src/Middleware/IdleConnectionMiddleware.php @@ -0,0 +1,37 @@ + */ + private array $ttlByConnection; + private string $connectionName; + + /** + * @param ArrayObject $connectionExpiries + * @param array $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 + { + /** @psalm-suppress InternalClass,InternalMethod */ + return new IdleConnectionDriver($driver, $this->connectionExpiries, $this->ttlByConnection[$this->connectionName], $this->connectionName); + } +} diff --git a/tests/DependencyInjection/AbstractDoctrineExtensionTest.php b/tests/DependencyInjection/AbstractDoctrineExtensionTest.php index 58c0fcf3c..60daad648 100644 --- a/tests/DependencyInjection/AbstractDoctrineExtensionTest.php +++ b/tests/DependencyInjection/AbstractDoctrineExtensionTest.php @@ -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'], ); @@ -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') @@ -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') @@ -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') diff --git a/tests/DependencyInjection/Compiler/MiddlewarePassTest.php b/tests/DependencyInjection/Compiler/MiddlewarePassTest.php index 06dc92cb5..acb64f930 100644 --- a/tests/DependencyInjection/Compiler/MiddlewarePassTest.php +++ b/tests/DependencyInjection/Compiler/MiddlewarePassTest.php @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 */ @@ -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 diff --git a/tests/DependencyInjection/DoctrineExtensionTest.php b/tests/DependencyInjection/DoctrineExtensionTest.php index 1468a65d4..904b5ad3f 100644 --- a/tests/DependencyInjection/DoctrineExtensionTest.php +++ b/tests/DependencyInjection/DoctrineExtensionTest.php @@ -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] diff --git a/tests/Middleware/IdleConnectionMiddlewareTest.php b/tests/Middleware/IdleConnectionMiddlewareTest.php new file mode 100644 index 000000000..40527a952 --- /dev/null +++ b/tests/Middleware/IdleConnectionMiddlewareTest.php @@ -0,0 +1,29 @@ + time() - 30, 'connectiontwo' => time() + 40]); + $ttlByConnection = ['connectionone' => 25, 'connectiontwo' => 60]; + + $middleware = new IdleConnectionMiddleware($connectionExpiries, $ttlByConnection); + $middleware->setConnectionName('connectionone'); + + $driverMock = $this->createMock(Driver::class); + $wrappedDriver = $middleware->wrap($driverMock); + + $this->assertInstanceOf(IdleConnectionDriver::class, $wrappedDriver); + } +}