PHP 8 compatibility

Issue #69 resolved
程巍 created an issue

test code

  1. composer require workerman/workerman
  2. Fill code into test.php
  3. php test.php start
<?php

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

use Workerman\Worker;

global $worker;
$worker = new Worker();
Worker::$pidFile = __DIR__.'/work.pid';
$worker->onWorkerStart = function () {
    global $worker, $sec;
    $sec = 0.5;
    \Workerman\Lib\Timer::add($sec, function () {
        exit();
    }, true);
};

// 运行worker
Worker::runAll();

Execute the above code at php8

PHP 8.0.11 (cli) (built: Sep 23 2021 20:15:09) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.11, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.11, Copyright (c), by Zend Technologies
bash-5.1# php test.php start
Workerman[test.php] start in DEBUG mode
------------------------------------- WORKERMAN --------------------------------------
Workerman version:4.0.19          PHP version:8.0.11
-------------------------------------- WORKERS ---------------------------------------
proto   user            worker          listen          processes    status
tcp     root            none            none            1             [OK]
--------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.

Warning: EventBase::loop(): Failed to invoke event callback in /app/vendor/workerman/workerman/Events/Event.php on line 195

Warning: EventBase::loop(): Failed to invoke event callback in /app/vendor/workerman/workerman/Events/Event.php on line 195

Warning: EventBase::loop(): Failed to invoke event callback in /app/vendor/workerman/workerman/Events/Event.php on line 195

Execute the above code at php7

PHP 7.4.14 (cli) (built: Jan 16 2021 00:10:32) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.14, Copyright (c), by Zend Technologies
bash-5.1# php test.php start
Workerman[test.php] start in DEBUG mode
------------------------------------- WORKERMAN --------------------------------------
Workerman version:4.0.19          PHP version:7.4.14
-------------------------------------- WORKERS ---------------------------------------
proto   user            worker          listen          processes    status
tcp     root            none            none            1             [OK]
--------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.
Worker[39] process terminated
Worker[40] process terminated
Worker[41] process terminated
Worker[42] process terminated

Comments (16)

  1. Ruslan Osmanov repo owner

    @程巍 , I don’t know what happens there yet, but
    1) it is a bad idea to exit() from an event callback because the other event objects (including the loops) have no opportunity to gracefully handle the shutdown. Yes, destructors are still called, but what if the other callbacks are being executed at the moment you call exit()? There is also no guarantee of any specific order of __destruct calls. It would be better to keep track of some global flag, probably send an "exit" signal to all events, wait for graceful termination for some time, then try to stop them forcefully.
    2) Are you forking processes after creation of the loop? I think you might not want to have a copy of the loop in the child processes. Anyway, the purpose of exit() is an abrupt process termination, and I don’t see why would anybody want to call it from the event callback directly.

    Anyway, the main problem is the need to debug the Workerman app in order to understand how are you using the event extension, and I don’t have a lot of time, sorry. If you could create a minimal reproducible example, things could be solved much faster.

  2. popo

    HI Ruslan Osmanov.

    I also encountered this problem when using php8 (php8.0.11/8.1.0RC2, event 3.0.6).

    Here is a minimal reproducible example.

    <?php
    ini_set('display_errors', 'on');
    error_reporting(E_ALL);
    $base = new EventBase();
    $client = stream_socket_client('tcp://example.com:80');
    $event = new Event($base, $client, Event::READ|Event::PERSIST, function($client){
        exit("EXIT HERE\n\n"); 
        // The same result for throws Error or Exception which codes may like this
        // $num = 1/0;
    }, $client);
    $event->add();
    fwrite($client, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
    $base->loop();
    

    The results for PHP7 and PHP5.

    $ php test.php
    EXIT HERE
    

    The results for PHP8.

    $ php test.php
    EXIT HERE
    
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    
    Warning: EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    PHP Warning:  EventBase::loop(): Failed to invoke event callback in /home/test.php on line 13
    

    For php8, an infinite loop will display errors and the CPU consumption is so high.

    I understand that it's a bad idea to call exit() or throw exceptions in event loop callback. However, many frameworks use event extensions as base, such as workerman reactphp amphp etc. These frameworks have many users who don't know much about the event extension. They may call exit() or fail to catch some exceptions or errors in the their business code, which will lead to an infinite loop and unavailable services.

  3. Ruslan Osmanov repo owner

    Hi @popo,

    Thank you for the minimal example! Well, pecl-event has never handled neither exit() nor exceptions in any special way. The changes we see in PHP 8 come from PHP core, and we, apparently, have to deal with them.

    One way to solve the issue is to delete (or stop) the event if the callback invocation fails. This would result in the following output for your test case:

    EXIT HERE
    
    
    Warning: EventBase::loop(): Failed to invoke event callback. Deleting event. in Standard input code on line 12
    

    However, just deleting events from the loop may not be sufficient because exceptions such as DivisionByZeroError would not be bubbled up to the caller of Event::loop. Then we might also throw EventException so that the caller is able to catch it.

    Please share your thoughts.

  4. popo

    I think it's a good idea to be consistent with the default behavior of PHP. That is, when exit() is called, destroy EventLoop and exit the process. When an exception or error is not caught, throw them, then destroy the EventLoop and exit the process. I think this is also the result that most developers can accept and anticipate in advance.

    Another idea is to throw an EventException when exit() is called or an error/exception is generated to inform the EventLoop caller of what happened and let him decide what to do.

  5. Ruslan Osmanov repo owner

    @popo, @程巍, I’ve tried to fix this issue in the master branch. Please check it out.

    You can either build the extension manually, or use the Docker and Docker Compose files, e.g.

    cat test.php | docker-compose run php8test ./run.sh
    

    If everything is okay, I’ll publish a new beta release after a while.

  6. Ruslan Osmanov repo owner

    It would be great if you could test it with PHP 7 as well, because I’ve applied the same logic to the PHP 7 version for consistency.

  7. popo

    The results for php7

    It works fine for php7.4.24. But cat test.php | docker-compose run php7test ./run.sh` has an error.

  8. Ruslan Osmanov repo owner

    @popo, Thank you.

    It works fine for php7.4.24. But cat test.php | docker-compose run php7test ./run.sh` has an error.

    The assertion error occurs because of the fact that the event stream argument has to be put into non-blocking mode. We could do this in the extension implicitly, but I think it is the user’s responsibility. (See stream_set_blocking() function.)

    So, apart from that, do you think that the new behavior works correctly?

  9. Log in to comment