After a fresh install of D8 and enabling simpletest, I am greeted with this error when visiting /admin/config/development/testing

UnexpectedValueException: RecursiveDirectoryIterator::__construct(/drupal/core/./modules/image/tests): failed to open dir: Too many open files in RecursiveDirectoryIterator->__construct() (line 107 of core/vendor/phpunit/php-file-iterator/File/Iterator/Factory.php).

Using OSX 10.9.2 (Mavericks) with PHP 5.4.17

After searching, I found this thread on Stack Overflow:
http://stackoverflow.com/questions/19766745/apache-php-osx-mavericks-fai...

Following the directions, I edited my /etc/launchd.conf file and added the following:

limit maxfiles 16384 32768

I then restarted the machine.

Note: you should set these values to ones that are appropriate for your machine. Setting them too high may cause problems. The default for OSX is 256. Note also that if you are setting these, you must set explicit numeric values. Setting to 'unlimited' will not take and the machine will remain at its defaults.

Not sure if this is something we can fix in Drupal but thought it wise to report in case others find a similar issue.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Category: Support request » Bug report
Priority: Normal » Major
Status: Active » Needs review
Issue tags: +DX (Developer Experience), +Performance, +beta target
FileSize
1.45 KB

This is at least major. Just like we don't want things to be broken out of the box on Windows, we also don't want that on OS X.

This is a bug that causes >256 files to be opened simultaneously. Even if that's lower than the limit on most OSes, that's still a bug.
It only happens when running tests. Possibly only when using run_tests.sh.

I did all sorts of debugging: print stack traces, using a PHP debugger, using lsof, and so on. The key aspect is that the SplFileInfo object is being passed around and seems to come with a built-in, hidden file pointer. That must be the case, because it's the only possible explanation for us reaching the max open file limit.
It's being passed around as a (premature?) optimization to save time later on, to not have to create those objects later on. But my preliminary testing showed that it is not used at all! At least not for regular kernel booting, nor for running a test using run-tests.sh (found by adding debug output followed by an exit; at the beginning of Extension::__call()).
Therefore it should be safe to not pass on the SplFileInfo object. That actually already was safe, but if my testing was correct and representative, it should also not cause any performance regression.


I also found two bugs that I had to fix as part of this solution:

  1. the opened file was not being closed.
  2. getSubPath() was being called on SplFileInfo, but that's a non-existing method! It's a method that only exists on a the iterator.
Wim Leers’s picture

Title: UnexpectedValueException: Too many open files in RecursiveDirectoryIterator->__construct() » run_tests.sh breaks on OS X ("Too many open files")
Wim Leers’s picture

Title: run_tests.sh breaks on OS X ("Too many open files") » run-tests.sh broken on OS X ("Too many open files")
sun’s picture

Too many open files in RecursiveDirectoryIterator->__construct() (line 107 of core/vendor/phpunit/php-file-iterator/File/Iterator/Factory.php

PHPUnit's file iterator opens plenty of directories and files that are only filtered later on, instead of within the filesystem recursion already.

http://www.php.net/manual/en/class.recursivedirectoryiterator.php#114504

getSubPath() was being called on SplFileInfo, but that's a non-existing method

The value is not a SplFileInfo object, but an instance of RecursiveDirectoryIterator, which provides sub-path methods.

http://www.php.net/manual/en/recursivedirectoryiterator.getsubpath.php

SplFileInfo object is being passed around and seems to come with a built-in, hidden file pointer.

SplFileInfo is a resource that holds the file information only.

ExtensionDiscovery::scanDirectory() does open the .info.yml files via SplFileObjects though. Theoretically, the file handle should be closed, because the local variable is replaced with a new one, but perhaps PHP doesn't behave correctly with regard to that.

Damien Tournoud’s picture

The problem is that RecursiveDirectoryIterator is used in \FilesystemIterator::CURRENT_AS_SELF, in which it returns the iterator itself when iterating.

So, here:

foreach ($iterator as $key => $fileinfo) {
}

$fileinfo is not a standalone SplFileInfo but the current directory iterator itself. So, when we set it to $extension->setSplFileInfo($fileinfo);, we are actually leaking this iterator. The directory handle for every visited directory stays open, and eventually we hit the maximum file number.

(This has nothing to do with files, we are actually leaking handles to directories.)

Also, Wim is correct that ExtensionDiscovery:scan() is wrong in calling ->getSubPath() because this method is part of DirectoryIterator, not SplInfo itself.

(And Extension is a proxy to the underlying SplInfo, who designed this mess?)

Damien Tournoud’s picture

FileSize
1.2 KB

Creating a standalone SplFileInfo should solve this nicely, but I haven't really tested this.

Wim Leers’s picture

#4: I tried precisely that. It's insufficient. The problem persists with only that change.

#5/#6: Thanks! :)

damiankloip’s picture

This fix looks good, and correct.

sun’s picture

Component: simpletest.module » extension system
FileSize
2.06 KB

OK, #5 makes sense to me. Sorry, I was not aware that RecursiveDirectoryIterator keeps the directory handle open after calling accept() and getChildren(). I could be wrong, but somehow that sounds like a PHP core bug to me.

In that case, we can remove the injection of SplFileInfo entirely. Extension only proxies to SplFileInfo if an actual file info method is called; e.g., getMTime(). This happens in e.g. _system_rebuild_module_data() and in other places.

The only info that is needed for sorting purposes is the subpath from which the extension originated. For now, we can simply add that as a property like the existing $origin in the loop — we can clean those up in a separate issue. I already wasn't really happy with tucking the $origin property onto the object, but ran out of energy in the original issue. Cleaning up those internals would only delay this bug fix, so let's do that in a separate issue.

Wim Leers’s picture

Status: Needs review » Needs work

#9 fixes the problem. I'd RTBC, but unfortunately this wouldn't pass the docs gate:

+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -380,10 +380,9 @@ protected function scanDirectory($dir, $include_tests) {
       // Track the originating directory for sorting purposes.
+      $extension->subpath = $fileinfo->getSubPath();
       $extension->origin = $dir;

There is no comment for $extension->subpath, nor is Extension::subpath documented in Extension.

sun’s picture

Status: Needs work » Needs review
Issue tags: -d8 dev environment, -Performance, -beta target
benjy’s picture

Status: Needs review » Reviewed & tested by the community

This does indeed fix the issue (which has been driving me crazy)

Since we have a follow up for the things mentioned in #10 this is RTBC

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Great work sleuthing on this.

Committed and pushed to 8.x. Thanks!

Wim Leers’s picture

YAY!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

benjy’s picture

This issue is back for me when running tests locally. Not sure if it's related but I recently updated to Yosemite.

PHP 5.5.18
Apache 2.4.9

jhedstrom’s picture

I've started to see this popping up again as well. Temporarily disabling xdebug seems to resolve the issue.

benjy’s picture

@jhedstrom, i'm not sure this is run-tests fault entirely, it just adds to the problem.

Since upgrading to Yosemite i've had the issue back, I think this is the right solution: http://docs.basho.com/riak/latest/ops/tuning/open-files-limit/#Mac-OS-X but i've not tried it yet.

Also note, restarting apache fixes the issue for me when it happens.