amphp / amp

A non-blocking concurrency framework for PHP applications. 🐘
https://amphp.org/amp
MIT License
4.25k stars 257 forks source link

stream_select hangs forever when FDs > 1024 are used #398

Closed michz closed 2 years ago

michz commented 2 years ago

amphp version: 2.6.2 parallel-functions version: 1.1.0 PHP version: tested with 7.4 and 8.1


Hi at all,

First of all: I know that stream_select does not work with file descriptors > 1024.

Nevertheless, I expected amphp to fail cleanly when problems with file descriptors > 1024 are encountered (see #338 ).

Lately I encountered a case in production that caused some confusion on my end. I managed to break it down to the following minimal scenario:

<?php
use Amp\Parallel\Worker\DefaultPool;
use Amp\Promise;
use function Amp\ParallelFunctions\parallel;

require_once "vendor/autoload.php";

# Open some bloating file descriptors
$fds = [];
for ($i = 0; $i < 1023; $i++) {
    $fds[] = fopen("/tmp/__0_a_evil_tmpfile.$i", 'w');
}

# Parallel Part
$promises = [];
$pool = new DefaultPool(1);
echo "1\n";
for ($i = 1; $i <= 5; $i++) {
    echo "2.$i\n";
    $promises[] = parallel(
        function () use ($i) {
            for ($j = 1; $j <= 10; $j++) {
                echo "Loop $i.$j\n";
                usleep(100000);
            }
        }, $pool
    )();
}

Promise\wait(Promise\all($promises));

The result is reproducible:

$ php main.php
My PID: 17
1
2.1
2.2
2.3
2.4
2.5

And then it hangs forever.

Doing an strace on the process shows, that it is waiting for a select without timeout and without any file descriptors it should check:

$ sudo strace -s1024 -tt -fp <my_pid>
strace: Process <my_pid> attached
10:35:18.901507 select(1024, [], [], [], NULL

The problem occurs regardless of the number of runners in the pool.

Using xdebug, I can trace it down until:

I'm am kind of cueluess, to be honest. Especially, because I also saw processes correctly dying with this error instead of hanging. I could not, however, sort out under which exact circumstances it hangs and under which it fails correctly.

Does anybody have an idea what happens here? Might this be a bug in amphp or even php?

I could not trace it down further until now, sorry…


I have created a gist that contains the setup I used for reproduction: https://gist.github.com/michz/6cc16886693fa81518a4fd6e0e638177

bwoebi commented 2 years ago

The problem lies within stream_select() itself, that it a) does not abort when an exception is thrown, but continues with an empty syscall (you'd see the exception if stream_select() were to return later on), and b) counts the omitted file descriptors (and if that counter is greater than zero, it goes ahead with the select(2) syscall, even if nothing is in it).

Given that this is not an AMP bug, I have to close this issue.

bwoebi commented 2 years ago

To reproduce the hang:

<?php

$fds = [];
for ($i = 0; $i < 1023; $i++) {
    $fds[] = fopen("/tmp/test/__0_a_evil_tmpfile.$i", 'w');
}

list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

$r = [$a];
$w = $e = [];
var_dump(stream_select($r, $w, $e, PHP_INT_MAX));

And then you can use set_error_handler(function() { throw new \Exception; }); to "hide" the exception.

bwoebi commented 2 years ago

I've re-reported it as https://github.com/php/php-src/issues/9590.

michz commented 2 years ago

Agree, thanks :+1:

kelunik commented 2 years ago

@bwoebi @michz Thanks for digging into this and opening the upstream issue!