symfony: [HttpClient] Failed to open stream: Too many open files

Symfony version(s) affected

4.5.2 / 5.3.13

Description

When launching PHPUnit test suite, I have this error:

PHPUnit 9.5.11 by Sebastian Bergmann and contributors.

Testing 
.............................................................   61 / 4733 (  1%)
.............................................................  122 / 4733 (  2%)
.............................................................  183 / 4733 (  3%)
.............................................................  244 / 4733 (  5%)
.............................................................  305 / 4733 (  6%)
.............................................................  366 / 4733 (  7%)
.............................................................  427 / 4733 (  9%)
.............................................................  488 / 4733 ( 10%)
.............................................................  549 / 4733 ( 11%)
.............................................................  610 / 4733 ( 12%)
.............................................................  671 / 4733 ( 14%)
.............................................................  732 / 4733 ( 15%)
................PHP Warning:  include(/var/www/project/symfony/vendor/phpunit/phpunit/src/Framework/Error/Warning.php): failed to open stream: Too many open files in /var/www/project/symfony/vendor/symfony/error-handler/DebugClassLoader.php on line 349
PHP Warning:  include(): Failed opening '/var/www/project/symfony/vendor/composer/../phpunit/phpunit/src/Framework/Error/Warning.php' for inclusion (include_path='.:/usr/share/php:/var/www/project/symfony/vendor/deployer/recipes') in /var/www/project/symfony/vendor/symfony/error-handler/DebugClassLoader.php on line 349
PHP Warning:  include(/var/www/project/symfony/vendor/phpunit/phpunit/src/Framework/Error/Warning.php): failed to open stream: Too many open files in /var/www/project/symfony/vendor/composer/ClassLoader.php on line 571
PHP Warning:  include(): Failed opening '/var/www/project/symfony/vendor/composer/../phpunit/phpunit/src/Framework/Error/Warning.php' for inclusion (include_path='.:/usr/share/php:/var/www/project/symfony/vendor/deployer/recipes') in /var/www/project/symfony/vendor/composer/ClassLoader.php on line 571
Class 'PHPUnit\Framework\Error\Warning' not found

The issue is related to symfony/http-client, no more issue when reverting these 3 files to v5.3.12 :

I tried to set up a reproducer but it seems the issue occurs only with projects having thousands of tests (4733 tests, 31650 assertions for my project).

How to reproduce

Upgrade to Symfony 5.3.13 and launch a large PHPUnit test suite with symfony/http-client use.

Possible Solution

No response

Additional Context

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 6
  • Comments: 33 (27 by maintainers)

Commits related to this issue

Most upvoted comments

Same as @javer I can confirm that any of those commits are the culprit.

I tested it on PHP 8.0.13 and PHP 8.0.14.

More precise bisecting (with fixing incompatible types in HttpClientDataCollector in intermediate commits) led me to the single first bad commit: https://github.com/symfony/http-client/commit/d05b20ee7e0214f50a4d7a5ba132dc56dd60547b

@nicolas-grekas Unfortunately the patch didn’t help. But my reproducer shows that we even don’t need to make any curl requests, because CurlClientState::reset() is called by ServicesResetter, so just having any HttpClient in the container is enough to get an error after many calls to ServicesResetter::reset(), see backtrace:

array:52 [
  0 => array:5 [
    "file" => "vendor/symfony/http-client/CurlHttpClient.php"
    "line" => 325
    "function" => "reset"
    "class" => "Symfony\Component\HttpClient\Internal\CurlClientState"
    "type" => "->"
  ]
  1 => array:5 [
    "file" => "vendor/symfony/http-client/TraceableHttpClient.php"
    "line" => 94
    "function" => "reset"
    "class" => "Symfony\Component\HttpClient\CurlHttpClient"
    "type" => "->"
  ]
  2 => array:5 [
    "file" => "vendor/symfony/http-kernel/DependencyInjection/ServicesResetter.php"
    "line" => 47
    "function" => "reset"
    "class" => "Symfony\Component\HttpClient\TraceableHttpClient"
    "type" => "->"
  ]
  3 => array:5 [
    "file" => "vendor/symfony/http-kernel/Kernel.php"
    "line" => 116
    "function" => "reset"
    "class" => "Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter"
    "type" => "->"
  ]
  4 => array:5 [
    "file" => "vendor/symfony/http-kernel/Kernel.php"
    "line" => 197
    "function" => "boot"
    "class" => "Symfony\Component\HttpKernel\Kernel"
    "type" => "->"
  ]
  5 => array:5 [
    "file" => "vendor/symfony/http-kernel/HttpKernelBrowser.php"
    "line" => 65
    "function" => "handle"
    "class" => "Symfony\Component\HttpKernel\Kernel"
    "type" => "->"
  ]
  6 => array:5 [
    "file" => "vendor/symfony/framework-bundle/KernelBrowser.php"
    "line" => 182
    "function" => "doRequest"
    "class" => "Symfony\Component\HttpKernel\HttpKernelBrowser"
    "type" => "->"
  ]
  7 => array:5 [
    "file" => "vendor/symfony/browser-kit/AbstractBrowser.php"
    "line" => 398
    "function" => "doRequest"
    "class" => "Symfony\Bundle\FrameworkBundle\KernelBrowser"
    "type" => "->"
  ]
  8 => array:5 [
    "file" => "vendor/symfony/browser-kit/AbstractBrowser.php"
    "line" => 606
    "function" => "request"
    "class" => "Symfony\Component\BrowserKit\AbstractBrowser"
    "type" => "->"
  ]
...
]

For example, in my application (test run) ServicesResetter::reset() is called 502 times before getting the error Failed to open stream: Too many open files, at the same time there weren’t any calls to CurlResponse constructor, so it seems we cannot rely on the response to free the resources.

@nicolas-grekas Here is a small isolated script to reproduce the issue (need lsof util and pcntl extension for verbose output):

<?php

use Symfony\Component\HttpClient\Internal\CurlClientState;

require dirname(__DIR__) . '/vendor/autoload.php';

$verbose = false;
$iterations = ceil(shell_exec('ulimit -n') / 2);

$state = new CurlClientState(6, 50);

for ($i = 0; $i < $iterations; $i++) {
    $state->reset();

    if ($verbose) {
        $output = [];
        exec('lsof -p ' . posix_getpid(), $output);
        var_dump(count($output));
    }
}

exec('ls');

results in:

PHP Warning:  exec(): Unable to fork [ls] in http_client_test.php on line 22

Changing $verbose to true exposes the stream handles leak 2 per each reset call:

int(136)
int(138)
...
int(1144)
int(1146)
int(1148)
PHP Warning:  exec(): Unable to fork [lsof -p 180109] in http_client_test.php on line 17
int(0)
PHP Warning:  exec(): Unable to fork [lsof -p 180109] in http_client_test.php on line 17
int(0)
PHP Warning:  exec(): Unable to fork [lsof -p 180109] in http_client_test.php on line 17
int(0)
PHP Warning:  exec(): Unable to fork [lsof -p 180109] in http_client_test.php on line 17
int(0)
PHP Warning:  exec(): Unable to fork [lsof -p 180109] in http_client_test.php on line 17
int(0)
PHP Warning:  exec(): Unable to fork [ls] in http_client_test.php on line 22

@jtojnar ok, so you’re experiencing something else. Please open an issue with as many details as possible when you have some hints.