Php 7.1 + Pecl-event + libevent — зависает в странном случае

Основываясь на этом ответе, я переключился на библиотеку pecl-event. Теперь у меня есть:

[root]# php -v
PHP 7.1.12 (cli) (built: Nov 22 2017 08:40:02) ( NTS ) Copyright (c) 1997-2017 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologieswith Zend OPcache v7.1.12, Copyright (c) 1999-2017, by Zend Technologies 
[root]# php --info | grep event
/etc/php.d/event.ini event libevent2 headers version => 2.1.8-stable
[root]# pecl list
Installed packages, channel pecl.php.net:
=========================================
Package Version State
event   2.3.0   stable

Пример ниже ведет себя странно. Если $loop->run() вызывается из функции runme(), она работает и вызывается обратный вызов. Но если $loop->run() вызывается из-за пределов runme(), он зависает!

require_once __DIR__.'/../vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();
//$loop = new \React\EventLoop\StreamSelectLoop();

runme($loop, $inner);

if (!$inner) {
    echo "Outer start\n";
    $loop->run();
}

function runme(\React\EventLoop\LoopInterface $loop, $inner)
{
    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://127.0.0.1:3306', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);

    $loop->addWriteStream($socket, function ($socket) use ($loop) {
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    });

    if ($inner) {
        echo "Inner start\n";
        $loop->run();
    }

    echo "Exit runme\n";
}

Результаты запуска:

[root@vultr Scraper]# php ./tests/test.php --inner
Inner start
done  false
Exit runme
[root@vultr Scraper]# php ./tests/test.php 
Exit runme
Outer start
...............HANGING HERE...........

Я что-то упустил или это проблема с одной из библиотек/PHP? У кого-нибудь есть опыт запуска php7.1 + react + libevent?

ОБНОВЛЕНИЕ: =============================================== =====================

Я провел тест с последней библиотекой «реагировать/сокет» «0.8.6».

require_once __DIR__.'/vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();

$connector = new React\Socket\Connector($loop);

runme($loop, $connector, $inner);

if (!$inner) {
    echo "Outer start\n";
    $loop->run();
}

function runme(\React\EventLoop\LoopInterface $loop, React\Socket\Connector $connector, $inner)
{
    $connector->connect('tcp://127.0.0.1:3306')->
    then(function (\React\Socket\ConnectionInterface $conn) {
        echo ("Hello MySQL!\n");
        $conn->close();
    },function ($e) {
        echo ("Bye MySQL!\n");
    })->done();

    if ($inner) {
        echo "Inner start\n";
        $loop->run();
    }

    echo "Exit runme\n";
}

он работает правильно и возвращает:

$ php ./testMysql.php 
Exit runme
Outer start
Hello MySQL!
$ php ./testMysql.php  --inner
Inner start
Hello MySQL!
Exit runme

Но если вы зайдете в \React\Socket\TcpConnector::waitForStreamOnce() и удалите функцию $canceller в новом объекте Promise, как показано ниже, он снова зависнет. Похоже, это работает в последней версии реакции как бы случайно, так как сокет не хранится очевидным образом, и фактически похож на код в версии 0.4.6.

private function waitForStreamOnce($stream)
    {
        $loop = $this->loop;

        return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream) {
            $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject) {
                $loop->removeWriteStream($stream);

                // The following hack looks like the only way to
                // detect connection refused errors with PHP's stream sockets.
                if (false === stream_socket_get_name($stream, true)) {
                    fclose($stream);

                    $reject(new \RuntimeException('Connection refused'));
                } else {
                    $resolve(new Connection($stream, $loop));
                }
            });
        });
    }



$ php ./testMysql.php  --inner
Inner start
.....HANGING
$ php ./testMysql.php 
Exit runme
Outer start
...HANGING

person Dmitry Pismennyy    schedule 29.11.2017    source источник


Ответы (2)


Проблема в том, что переменная $socket уничтожается при возврате runme() (как и любая локальная переменная PHP!). В результате соединение, открытое на этом сокете, закрывается.

Расширение Event делает все возможное для предотвращения утечек памяти, поэтому по возможности не сохраняет ссылки на пользовательские переменные. В частности, все методы, принимающие ресурс сокета (Event::__construct, например) только получить базовый числовой файловый дескриптор из входных данных переменные. Фактически пользователь отвечает за поддержание этих активных переменных.

Следующий скрипт устраняет проблему, перемещая $socket в глобальную область.

require_once 'vendor/autoload.php';

$inner = count($argv) > 1;
$loop = new \React\EventLoop\ExtEventLoop();
$socket = init_socket();

runme($loop, $socket, $inner);

if (!$inner) {
    echo "Outer start\n";
    $loop->run();
}

function init_socket()
{
    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://test.local:80', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);
    return $socket;
}

function runme(\React\EventLoop\LoopInterface $loop, $socket, $inner)
{
    $loop->addWriteStream($socket, function ($socket) use ($loop) {
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    });

    if ($inner) {
        echo "Inner start\n";
        $loop->run();
    }

    echo "Exit runme\n";
}

В реальном приложении вы, вероятно, сохранили бы $socket как переменную-член класса.

person Ruslan Osmanov    schedule 30.11.2017
comment
Как я уже писал, этот код является частью библиотеки react/socket-client версии 0.4.6, которая отлично работает в php5.6. Похоже, мне нужно найти обновление этой библиотеки в моем приложении. - person Dmitry Pismennyy; 30.11.2017
comment
@DmitryPismennyy, даже если скрипт работал должным образом на PHP 5.6, это было только из-за поведения GC, которое считалось, что не уничтожает $socket. Я описал корень проблемы: переменная $socket должна быть сохранена(!); в противном случае соединение будет потеряно, и цикл будет вести себя непредсказуемо (может зависнуть, может segfault... чего ожидать от программы, если она оперирует закрытым файловым дескриптором?). Эта идея применима и к PHP 7. - person Ruslan Osmanov; 30.11.2017
comment
Я не имею в виду обновление о рабочем коде в PHP 5.6. Я имел в виду обновление в моем основном посте в соответствии с последней библиотекой реагирования, которая работает правильно, случайно на первый взгляд. - person Dmitry Pismennyy; 30.11.2017
comment
@DmitryPismennyy, вторая часть вашего вопроса выглядит как совершенно другой вопрос, поскольку вы начинаете использовать соединители React вместо явных сокетов. Кроме того, не очень понятно, как создается базовый поток, как он передается расширению Event и когда уничтожается. Чтобы найти ответы на эти вопросы, нужно провести серьезную отладку для разных версий React. И это, боюсь, несколько выходит за рамки SO. Я бы предпочел адресовать вторую часть вопроса разработчикам React в их системе отслеживания ошибок. - person Ruslan Osmanov; 30.11.2017

Привет, основной разработчик ReactPHP, только что просмотрел ваш скрипт, и я могу воспроизвести его локально, поэтому я собираюсь зарегистрировать проблему (хотя это может быть вне нашей компетенции) для этого.

require_once __DIR__.'/../vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();
//$loop = new \React\EventLoop\StreamSelectLoop();

runme($loop, $inner);

    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://127.0.0.1:3306', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);

    $loop->addWriteStream($socket, function ($socket) use ($loop) {
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    });

    if ($inner) {
        echo "Inner start\n";
        $loop->run();
    }

    echo "Exit runme\n";

Но в качестве альтернативы я предлагаю вам изучить наш компонент сокета для обработки подключений.

Что может выглядеть примерно так:

$loop = new \React\EventLoop\ExtEventLoop();
$connector = new React\Socket\Connector($loop);

$connector->connect('tcp://127.0.0.1:3306')->then(function (ConnectionInterface $conn) use ($loop) {
    $conn->write("Hello MySQL!\n");
});

$loop->run();
person WyriHaximus    schedule 29.11.2017
comment
Спасибо за помощь. На самом деле, я извлек вышеприведенный код из приложения, которое использует библиотеку react + react/mysql, основанную на \React\SocketClient\Connector(). Приложение отлично работает с php5.6 + libevent 2.0. Но после обновления до php7.1 я не могу совместить работоспособное решение с библиотеками событий. - person Dmitry Pismennyy; 30.11.2017
comment
Это очень распространенная проблема с асинхронным программированием и, в частности, с расширением Event. Пользователи обычно не понимают, что расширение не хранит ссылки на входные переменные, которые используются для настройки событий. (Параметр $data является особым.) С точки зрения пользователя может показаться удобным передать сокет только какому-то конструктору событий, а затем забыть об этом. Однако такое поведение также потребует от пользователя освобождения ссылок, когда они больше не нужны. И последний сценарий, на мой взгляд, делает асинхронное программирование еще более сложным. - person Ruslan Osmanov; 30.11.2017
comment
Как я уже писал, этот код является частью библиотеки react/socket-client версии 0.4.6, которая отлично работает в php5.6. Похоже, мне нужно обновить библиотеку. - person Dmitry Pismennyy; 30.11.2017