Based on this answer, I've switched to pecl-event library. Now I have:
[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
The example below behaves strangely. If $loop->run()
is called from within of runme()
function, it works and callback is called. But if $loop->run()
is called from outside of runme()
, it hangs!
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";
}
Run results:
[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...........
Do I miss something or is it an issue with one of the libraries/PHP? Does anybody have experience in running php7.1 + react + libevent?
UPDATE: ====================================================================
I did test with latest "react/socket" library "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";
}
it works proper and returns:
$ php ./testMysql.php
Exit runme
Outer start
Hello MySQL!
$ php ./testMysql.php --inner
Inner start
Hello MySQL!
Exit runme
But if you go into \React\Socket\TcpConnector::waitForStreamOnce() and remove $canceller function in new Promise object like below, than it hangs again. Looks like it works in latest version of react a kind of accidentally as socket not stored obvious way, and in fact similar to code in v0.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
The problem is that the $socket
variable gets destroyed when runme()
returns (just as any local PHP variable!). As a result, the connection opened on this socket is closed.
Event extension makes its best to prevent memory leaks, so it does not store references to the user variables if possible. Particularly, all methods accepting a socket resource (Event::__construct
, for instance) only retrieve the underlying numeric file descriptor from the input variables. The user is actually responsible for keeping those variables alive.
The following script fixes the issue by moving $socket
to the global scope.
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";
}
In a real application you would probably store $socket
as a class member variable.