Repository navigation
Every kernel reboot leaks the previous container since symfony/http-kernel 7.4.21 #250
Description
What are you trying to achieve?
Run a large functional suite (~3000 Cest tests, rebootable_client: true) within a fixed memory_limit.
What do you get instead?
Since symfony/http-kernel 7.4.21, every test that triggers a kernel reboot (any test sending a second request) keeps its previous container alive until the process ends. Our suite now exhausts its memory_limit after about 1000 tests.
Kernel::shutdown() now ends with gc_collect_cycles() (symfony/http-kernel 7.4.21, "Release the services of the container when the kernel shuts down"). Connector\Symfony::ensureKernelShutdown() calls $kernel->shutdown() from a closure bound to the old container:
The cycle collection therefore runs while the old container is still referenced as the closure's $this. Afterwards that container is never freed: a WeakReference to it stays valid for the rest of the run, even after explicit gc_collect_cycles() calls between tests. No entity manager is retained, only the container.
Measured with one Cest of 28 tests (debug: false), counting the containers that are still alive at the end of the suite through WeakReferences collected on every reboot:
| containers alive | |
|---|---|
| module-symfony 3.11.0 + http-kernel 7.4.21 | 22 |
same, with gc_collect_cycles() removed from Kernel::shutdown() |
3 |
same, with gc_disable()/gc_enable() around it |
22 |
same, ensureKernelShutdown() with a static closure that releases the old container before shutdown() |
3 |
The 3 remaining containers are the one created in _initialize() and the current one.
A change that fixes it for us, keeping the doctrine.connections handling introduced in #244 (the closure bound to the container was also introduced there):
protected function ensureKernelShutdown(): void
{
$this->kernel->boot();
$container = $this->kernel->getContainer();
$connections = Closure::bind(static function (object $container): mixed {
if (!property_exists($container, 'parameters') || !is_array($container->parameters)) {
return null;
}
$connections = $container->parameters['doctrine.connections'] ?? null;
unset($container->parameters['doctrine.connections']);
return $connections;
}, null, $container::class)($container);
$oldContainer = WeakReference::create($container);
unset($container);
try {
$this->kernel->shutdown();
} finally {
if ($connections !== null && ($container = $oldContainer->get()) !== null) {
Closure::bind(static function (object $container) use ($connections): void {
$container->parameters['doctrine.connections'] = $connections;
}, null, $container::class)($container);
}
}
}Details
- Codeception version: 5.3.7
- codeception/module-symfony: 3.11.0 (
mainhas the same code) - symfony/http-kernel: 7.4.21 (the
gc_collect_cycles()call is the only related change inKernel::shutdown()since 7.4.20) - PHP Version: 8.5.9 (ZTS, FrankenPHP image)
- Operating System: Linux (Docker)
- Installation type: Composer
- Suite configuration:
modules:
enabled:
- Symfony:
app_path: 'src'
environment: 'test'
- Doctrine:
depends: Symfony
cleanup: true
- REST:
depends: Symfony
url: /
config:
Symfony:
rebootable_client: true
Activity