0

We are trying to terminate a PHP script upon fatal error so the script can be restarted by Supervisor. The following is our exception handler:

    public function exceptionHandler(\Throwable $e)
    {
        if(Config::get('APP_DEBUG')) {
            echo (date("Y-m-d H:i:s").", uncaught exception in WsApp: ".$e->getMessage().PHP_EOL."");
        } else {
            \Sentry\captureException($e);
        }

        //exec('kill -9 ' . getmypid());
        exit("tried to exit from WsApp\r\n");
    }

The PHP script depends on MySQL server. If I stop MySQL server before running this PHP script, I get the following output in terminal:

2023-08-25 20:18:13, exception happened when trying to setupConn, SQLSTATE[HY000] [2002] No such file or directory
2023-08-25 20:18:13, uncaught exception in WsApp: Failed to setup connection to local DB.
tried to exit from WsApp // indication that error handler actually ran
^C // script continued to run until terminated by Ctrl + C

It seems exit in the error handler doesn't really terminal the script itself until I press Ctrl + C on keyboard.

The following is our code that throws exception upon failure to establish connection to DB server:

        try {
            $this->dbConn = new \PDO($dsn, Config::get('DB_USER_BI'), Config::get('DB_PW_BI'), $options);
        } catch (\Throwable $th) {
            if(Config::get('APP_DEBUG')) {
                echo date("Y-m-d H:i:s").", exception happened when trying to setupConn, ".$th->getMessage().PHP_EOL."";
            }
            // If a local DB connection cannot be established
            // Then there is no point to run any script
            throw new \Exception('Failed to setup connection to local DB.');
        }

But the following line as suggested by another post actually does the job.

exec('kill -9 ' . getmypid());

I get the following output in terminal:

2023-08-25 20:16:14, exception happened when trying to setupConn, SQLSTATE[HY000] [2002] No such file or directory
2023-08-25 20:16:14, uncaught exception in WsApp: Failed to setup connection to local DB.
Killed // output of exec(...)

My question is, why exit() doesn't terminate the script? My understanding is that exit will terminate the entire process.

Also what would be the consequence of running the following line? Memory leak because class destructor doesn't get to run?

exec('kill -9 ' . getmypid());

Please advise, thanks.

Zhiyong Li
  • 469
  • 3
  • 14
  • 1
    My guess is that it's trying to close its connection to the database, and this is hanging. – Barmar Aug 25 '23 at 20:37
  • Don't use `kill -9` if you can avoid it. Just use `kill`. This sends the same signal that typing Ctrl-C does. – Barmar Aug 25 '23 at 20:38
  • Hi @Barmar thanks for your quick response and suggestion. I just included our code to setup db connection. Do you think PDO will try to close connection even if a connection is not established? – Zhiyong Li Aug 25 '23 at 20:44
  • No, it shouldn't try to tear down a connection if it never established it in the first place. – Barmar Aug 25 '23 at 20:45
  • 1
    You may need to use a system call trace to see where it's stuck. – Barmar Aug 25 '23 at 20:46
  • Why don't you try with a minimal test script like the one given [in the documentation](https://www.php.net/manual/en/function.set-exception-handler.php#refsect1-function.set-exception-handler-examples)? – Olivier Aug 25 '23 at 20:47
  • @Olivier I actually tried a minimal version of of the script, and exit() was able to terminate that script. I will use system call trace as suggested by Barmar and see what I can find. – Zhiyong Li Aug 25 '23 at 21:08
  • @Barmar Being pedantic here, but `kill` will send a `SIGKILL` whereas a `ctrl+c` in terminal will send `SIGINT`. They are likely equivalent in this case, but it's useful to note that there are differences in case there's a user-defined signal handler hiding somewhere. Additional fun fact: It is nigh-impossible to send a SIGINT programmatically. It is, by definition, a human-generated signal. – Sammitch Aug 25 '23 at 22:08
  • Are you using php-fpm? Have you installed/enabled the `php-uopz` extension? `exit()` acts a bit differently in php-fpm and the `php-uopz` prevent the process termination but different solution are possible. – Alberto Fecchi Aug 25 '23 at 23:33
  • @AlbertoFecchi we don't have fpm nor uopz enabled,but we do use ev and zmq. But I doubt behavior of exit() has anything to do with system environment. Another script that only differs slightly terminates as expected. So it must be the script itself. – Zhiyong Li Aug 25 '23 at 23:47
  • 1
    Maybe is ZeroMQ that is still hanging for some reasons. Have you tried to set a value for `ZMQ::SOCKOPT_LINGER` (before `connect()`)? Something like this `$socket->setSockOpt(ZMQ::SOCKOPT_LINGER, 10);` https://www.php.net/manual/en/class.zmq.php#zmq.constants.sockopt-linger – Alberto Fecchi Aug 25 '23 at 23:57
  • @AlbertoFecchi The problem turned out to be really ZMQ, in a very bizarre way. After commenting out of various parts of the code and moving them around, it turned out if the code to establish DB connection is called after after first SOCKET_PULL is get, exit hangs. If DB connection code is called before SOCKET_PULL is get, then exit() works as expected. Quite weird. – Zhiyong Li Aug 26 '23 at 00:08

1 Answers1

1

After commenting out various blocks of code in the script, then moving them around, the problem is tracked down to something related to ZMQ.

If the code to setup DB connection is called before the 3rd line of code below:

$gwLoop = React\EventLoop\Loop::get();
$gwContext = new Context($gwLoop);
$msgAckPuller = $gwContext->getSocket(ZMQ::SOCKET_PULL);

Then exit() works as expected. If the code to setup DB connection is called after the 3rd line of code or any code like the following:

getSocket(ZMQ::SOCKET_PULL);

Then exit() will hang.

Calling DB connection code after any number of lines of code to

getSocket( ZMQ::SOCKET_PUSH)

appears to trigger exit() to work just fine.

Setting ZMQ::SOCKOPT_LINGER doesn't seem to prevent exit() from hanging:

$msgAckPuller->setSockOpt(ZMQ::SOCKOPT_LINGER, 10);

My knowledge about ZMQ and ReactPHP is limited, so I just close the question and leave the information above for future reference.

I reported this issue with a minimum test code to React's repo on GitHub.

use React\ZMQ\Context;

require dirname(__DIR__).'/vendor/autoload.php';

$testLoop = React\EventLoop\Loop::get();
$testContext = new Context($testLoop);

$testPusher = $testContext->getSocket(ZMQ::SOCKET_PUSH);
$testPusher->bind('tcp://127.0.0.1:5555');

//exit("exited after ZMQ:SOCKET_PUSH / before ZMQ::SOCKET_PULL\r\n");

$testPuller = $testContext->getSocket(ZMQ::SOCKET_PULL);

exit("tried to exit after ZMQ::SOCKET_PULL\r\n");

$testPuller->bind('tcp://127.0.0.1:5557');

Thanks everyone for participating in the discussion.

Zhiyong Li
  • 469
  • 3
  • 14