Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register the idle connection listener #1739

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"/>
alli83 marked this conversation as resolved.
Show resolved Hide resolved
</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

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

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm

MethodSignatureMismatch

src/Middleware/IdleConnectionMiddleware.php:10:7: MethodSignatureMismatch: Method Doctrine\Bundle\DoctrineBundle\Middleware\IdleConnectionMiddleware::wrap with return type 'Symfony\Bridge\Doctrine\Middleware\IdleConnection\Driver' is different to return type 'Doctrine\DBAL\Driver' of inherited method Doctrine\DBAL\Driver\Middleware::wrap (see https://psalm.dev/042)
ostrolucky marked this conversation as resolved.
Show resolved Hide resolved
{
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function __construct(ArrayObject $connectionExpiries, array $ttlByConnection)
public function __construct(ArrayObject $expiringConnections, array $ttlByConnection)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, this is a list of expiries and not connections. There were discussions regarding the naming of variables above, and it was decided to use connectionExpiries

{
$this->connectionExpiries = $connectionExpiries;
$this->ttlByConnection = $ttlByConnection;
}

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

public function wrap(Driver $driver): IdleConnectionDriver
{
return new IdleConnectionDriver($driver, $this->connectionExpiries, $this->ttlByConnection[$this->connectionName], $this->connectionName);
}
}
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