Skip to content

Every kernel reboot leaks the previous container since symfony/http-kernel 7.4.21 #250

Open

Description

@mazasb

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:

https://github.com/Codeception/module-symfony/blob/main/src/Codeception/Lib/Connector/Symfony.php#L70-L87

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 (main has the same code)
  • symfony/http-kernel: 7.4.21 (the gc_collect_cycles() call is the only related change in Kernel::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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions