swoole / swoole-src

🚀 Coroutine-based concurrency library for PHP
https://www.swoole.com
Apache License 2.0
18.27k stars 3.16k forks source link

\Swoole\Server::shutdown never gets called and signals don't seem to work with \Swoole\Process::signal() / pcntl_signal() #5323

Closed henrywood closed 2 weeks ago

henrywood commented 3 weeks ago

Please answer these questions before submitting your issue.

  1. What did you do? If possible, provide a simple script for reproducing the error.

I have this code

<?php
require __DIR__ . '/vendor/autoload.php';

use Xin\Swoole\Rpc\Server;
use Xin\Cli\Color;
use Xin\Swoole\Rpc\Handler\Handler;
use Xin\Swoole\Rpc\Enum;
use Xin\Swoole\Rpc\LoggerInterface;

/////////////////////////////////////////////////////////////////////////

define('RPCSERVER_RUN_AS_USER', 'henrik');
define('RPCSERVER_PORT', 11520);

/////////////////////////////////////////////////////////////////////////

$workerPID = new Swoole\Atomic(0);

/////////////////////////////////////////////////////////////////////////

function me() : string {
        return basename($_SERVER['argv'][0], (defined('EXT')) ? '.'.EXT : '.php');
}

function logger(string $msg) : void {
        echo $msg.PHP_EOL;
}

/////////////////////////////////////////////////////////////////////////

class RPCMethodDocumentation {

        public function __construct(public string $handlerName, private \ReflectionMethod $method) {}

                public function __toString() {

                        $comment = $this->method->getDocComment();
                        if (empty($comment)) $comment = '/**'.PHP_EOL.' * <NO DESCRIPTION>'.PHP_EOL.' */';
                        $comment = str_replace("\t", "", $comment);
                        $type = $this->method->getReturnType();
                        $type = ($type === NULL) ? 'void' : (string) $type;
                        $out = $comment.PHP_EOL.$this->method->name.'('.$this->renderParams().') : '.$type.PHP_EOL.PHP_EOL;
                        return $out;
                }

        private function renderParams() {

                $out = [];

                foreach($this->method->getParameters() as $refParam) {

                        $aNull = ($refParam->allowsNull()) ? '?' : '';
                        $str = $aNull.$refParam->getType().' $'.$refParam->getName();

                        if ($default = $refParam->getDefaultValue()) {
                                $str.= ' = '.$default;
                        }

                        $out[] = $str;
                }

                return implode(', ', $out);
        }
}

/////////////////////////////////////////////////////////////////////////

function scanForRPCHandlers() : array {

        $out = [];

        $classes = get_declared_classes();

        foreach($classes as $class) {

                $c = new \ReflectionClass($class);

                if ($c->isSubclassOf(Handler::class)) {

                        $handlerName = $class;

                        $methods = $c->getMethods(\ReflectionMethod::IS_PUBLIC);

                        foreach($methods as $m) {

                                if ($m->name == '__construct') continue;

                                if (! isset($out[$handlerName])) {
                                        $out[$handlerName] = [];
                                }

                                $out[$handlerName][$m->name] = new RPCMethodDocumentation($handlerName, $m);
                        }
                }
        }

        return $out;
}

///////////////////////////////////////////////////////////////////////////////////////////

class ElyRPCServer extends \Xin\Swoole\Rpc\Server {

        public function serve($host, $port, $config = []) {

                if (!extension_loaded('swoole')) {
                        echo Color::error('The swoole extension is not installed');
                        return;
                }

                if (!extension_loaded('signal_handler')) {
                        echo Color::error('The signal_handler extension is not installed');
                        return;
                }

                $this->host = $host;
                $this->port = intval($port);
                $this->config = $config;

                set_time_limit(0);
                $server = new \Swoole\Server($this->host, $this->port, SWOOLE_BASE);

                $server->set($config);

                $server->on('receive', [$this, 'receive']);
                $server->on('workerStart', [$this, 'workerStart']);
                $server->on('shutdown', [$this, 'shutdown']);
                $server->on('start', [$this, 'start']);

                $this->beforeServerStart($server);

                $server->start();
        }

        public function start($server) : void {

        }

        public function shutdown($server) : void {

                echo __METHOD__.PHP_EOL;

                $pidFile = '/var/run/'.me().'.pid';
                if (file_exists($pidFile)) unlink($pidFile);

                file_put_contents('/tmp/foo5', getmypid());

                // Unregister here with WSSERVER
                /*
                go(function () {

                        try {
                                $p = (int) constant(strtoupper(me()).'_PORT');
                                [$ip, $port] = explode(':', COMMON_WSSERVER_ADDRESS);
                                $client = new HttpClient($ip, $port);
                                $client->set(['timeout' => 3]);
                                $client->upgrade('/');
                                $ipAndPort = '-'.getLocalIP().':'.$p.'='.me().'-';
                                $client->push($ipAndPort);
                                $reply = $client->recv()->data;
                                logger(sprintf("De-registered at '%s' - Reply was: '%s' ", COMMON_WSSERVER_ADDRESS, $reply));
                                $client->close();
                        } catch(Exception) {}
                });
                 */

                $unixSocket = '/var/run/'.me().'.sock';
                if (file_exists($unixSocket)) unlink($unixSocket);

                logger('Shutting down ...');
                closelog();
        }

        public function workerStart($server, $workerId) {

                swoole_set_process_name(me().' [rpc] [worker]');

                global $workerPID;
                $workerPID->set(getmypid());

                // TODO

        }

        public function receive($server, $fd, $reactor_id, $data) {

                $data = trim($data);
                if ($this->debug) {
                        echo Color::colorize("fd:{$fd} data:{$data}", Color::FG_LIGHT_GREEN) . PHP_EOL;
                }
                try {
                        $data = json_decode($data, true);
                        $service = $data[Enum::SERVICE];
                        $method = $data[Enum::METHOD];
                        $arguments = $data[Enum::ARGUMENTS];

                        if (!isset($this->services[$service])) {
                                throw new RpcException('The service handler is not exist!');
                        }

                        $ref = new ReflectionClass($this->services[$service]);
                        $handler = $ref->newInstance($server, $fd, $reactor_id);
                        $result = $handler->$method(...$arguments);
                        $response = $this->success($result);
                        $server->send($fd, json_encode($response));

                        if ($this->logger && $this->logger instanceof LoggerInterface) {
                                $this->logger->info($data, $response);
                        }
                } catch (\Exception $ex) {
                        $response = $this->fail($ex->getCode(), $ex->getMessage());
                        $server->send($fd, json_encode($response));

                        if ($this->logger && $this->logger instanceof LoggerInterface) {
                                $this->logger->error($data, $response, $ex);
                        }
                }
        }

        public function beforeServerStart($server) {

                swoole_set_process_name(me().' [rpc] [master]');

                attach_signal(SIGTERM, function() use($server){

                        echo __LINE__;

                        $server->shutdown();
                        die(0);
                });

                if ($server->setting['daemonize']) {

                        attach_signal(SIGQUIT, function() use($server) {

                                posix_kill(getmypid(), SIGTERM);
                        });

                } else {

                        attach_signal(SIGINT, function() use($server){

                                echo "Shutting down II ...".PHP_EOL;
                                //posix_kill(getmypid(), SIGTERM);
                                $server->shutdown();
                                echo "X";
                                posix_kill(getmypid(), SIGKILL);
                        });
                }

                if (posix_isatty(\STDOUT)) {

                        $me = me();
                        echo Color::colorize("-----------------------------------------------------------------------------------", Color::FG_LIGHT_GREEN) . PHP_EOL;
                        echo Color::colorize("          {$me} :: RPC Server - Listening on TCP {$this->port}        ", Color::FG_CYAN , Color::FG_LIGHT_GREEN) . PHP_EOL;
                        echo Color::colorize("-----------------------------------------------------------------------------------", Color::FG_LIGHT_GREEN) . PHP_EOL;
                }
        }
}

class RPCLogger implements LoggerInterface {

        public function info($request, $response) {

                var_dump($request, $response);

                $info = '';

                // TODO
                logger('INFO [ RPC ] :: '.$info);
        }

        public function error($request, $response, Exception $ex) {

                // TODO

        }
}

/////////////////////////////////////////////////////////////////////////////////////////

class ElyRPCHandler extends Handler {

        public function __construct($server, $fd, $reactorId) {
                $this->server = $server;
                $this->fd = $fd;
                $this->reactorId = $reactorId;
        }
}

/////////////////////////////////////////////////////////////////////////////////////////

class TestHandler extends ElyRPCHandler {

        /**
         * Returns 'success' !
         */
        public function test() : string {

                return 'success';
        }

        public function test2(array $foo = []) {

                return array_reverse($foo);
        }

        private function foobar() {

                return 'HELLO';
        }
}

/////////////////////////////////////////////////////////////////////////////////////////

$handlers = scanForRPCHandlers();

//var_dump($handlers);

foreach($handlers as $handler => $methods) {

        echo $handler.PHP_EOL;
        echo "------------------------------------".PHP_EOL;

        foreach($methods as $method) {
                echo (string) $method.PHP_EOL;
        }
}

if (posix_getuid() !== 0) {
        echo "Error: Must be started as root\n";
        die(1);
}

$daemonize = FALSE;
$rpcDebug = FALSE;
$rpcService = 'test';
$rpcServiceClass = TestHandler::class;

$server = new ElyRPCServer();
$server->logger = new \RPCLogger();
$server->setDebug($rpcDebug);
$server->setHandler($rpcService, $rpcServiceClass);

$userInfo=posix_getpwnam(constant(strtoupper(me()).'_RUN_AS_USER'));
$userID=$userInfo["uid"];

$pidFile = '/var/run/'.me().'.pid';

touch($pidFile);
chmod($pidFile, 0777);
chown($pidFile, $userID);

// Become the proper user
posix_setuid($userID);

$port = (int) constant(strtoupper(me()).'_PORT');

// Start server
$server->serve('0.0.0.0', $port, [
        'log_file'                              => '/dev/null',
        //'pid_file'                            =>      $pidFile,
        'daemonize'                             =>      $daemonize,
        'max_request'                   =>      500,            // Total number of requests to be processed
        'open_eof_check'                =>      TRUE,
        'package_eof'                   =>      "\r\n",
]);

I tried using both pcntl_signal and \Swoole\Process::signal() but neither of them would work. I therefore use attach_signal from https://github.com/rstgroup/php-signal-handler (PHP Extension)

  1. What did you expect to see?

I expected $server->shutdown() to actually call ElyRPCServer::shutdown() and signals to be processed correctly.

  1. What did you see instead?

ElyRPCServer::shutdown() never gets called (file /tmp/foo5 is never written)

When pressing CTRL+C, this is the output:

^CShutting down II ... XShutting down II ... XKilled

As it can be seen, none of the echo of ElyRPCServer::shutdown() function is output

NOTE: The Xin\Swoole\Rpc comes from this github repo: https://github.com/limingxinleo/x-swoole-rpc/tree/master

How can I make it work correctly ?

  1. What version of Swoole are you using (show your php --ri swoole)?

swoole

Swoole => enabled Author => Swoole Team team@swoole.com Version => 5.1.0-dev Built => Jul 8 2023 19:03:10 coroutine => enabled with boost asm context epoll => enabled eventfd => enabled signalfd => enabled cpu_affinity => enabled spinlock => enabled rwlock => enabled openssl => OpenSSL 1.1.1f 31 Mar 2020 dtls => enabled http2 => enabled json => enabled pcre => enabled zlib => 1.2.11 mutex_timedlock => enabled pthread_barrier => enabled futex => enabled async_redis => enabled

Directive => Local Value => Master Value swoole.enable_coroutine => On => On swoole.enable_library => On => On swoole.enable_fiber_mock => Off => Off swoole.enable_preemptive_scheduler => Off => Off swoole.display_errors => On => On swoole.use_shortname => On => On swoole.unixsock_buffer_size => 8388608 => 8388608

  1. What is your machine environment used (show your uname -a & php -v & gcc -v) ?

Linux HSLAPTOP-ASUS 5.10.102.1-microsoft-standard-WSL2 #1 SMP Wed Mar 2 00:30:59 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux PHP 8.2.10 (cli) (built: Sep 2 2023 06:58:59) (NTS) Copyright (c) The PHP Group Zend Engine v4.2.10, Copyright (c) Zend Technologies Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none:hsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.2' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-9QDOt0/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)

NathanFreeman commented 2 weeks ago

Forcibly killing the process will not trigger onShutdown, such as kill -9 Use kill -15 to send the SIGTERM signal to the main process in order to terminate according to the normal process flow Pressing Ctrl+C in the command line will immediately stop the program, and onShutdown will not be called at the lower level

see https://wiki.swoole.com/en/#/server/events?id=onshutdown

henrywood commented 2 weeks ago

I understand, however I signal the master/first process using

kill -QUIT

but but what I then see is that the signal handler is being called (which then converts the signal to SIGTERM) but if you look closely on the output:

Shutting down II ... XShutting down II ... XKilled

You may see that the shutdown callback is not getting called even if you send SIGTERM

NathanFreeman commented 2 weeks ago

Certain signals, like SIGTERM and SIGALRM, cannot be set as monitored signals in Swoole\Server

henrywood commented 2 weeks ago

I got it to work like this:

<?php
require __DIR__ . '/vendor/autoload.php';

use Xin\Swoole\Rpc\Server;
use Xin\Cli\Color;
use Xin\Swoole\Rpc\Handler\Handler;
use Xin\Swoole\Rpc\Enum;
use Xin\Swoole\Rpc\LoggerInterface;

/////////////////////////////////////////////////////////////////////////

define('RPCSERVER_RUN_AS_USER', 'henrik');
define('RPCSERVER_PORT', 11520);

/////////////////////////////////////////////////////////////////////////

$workerPID = new Swoole\Atomic(0);

/////////////////////////////////////////////////////////////////////////

function me() : string {
        return basename($_SERVER['argv'][0], (defined('EXT')) ? '.'.EXT : '.php');
}

function logger(string $msg) : void {
        echo $msg.PHP_EOL;
}

/////////////////////////////////////////////////////////////////////////

class RPCMethodDocumentation {

        public function __construct(public string $handlerName, private \ReflectionMethod $method) {}

                public function __toString() {

                        $comment = $this->method->getDocComment();
                        if (empty($comment)) $comment = '/**'.PHP_EOL.' * <NO DESCRIPTION>'.PHP_EOL.' */';
                        $comment = str_replace("\t", "", $comment);
                        $type = $this->method->getReturnType();
                        $type = ($type === NULL) ? 'void' : (string) $type;
                        $out = $comment.PHP_EOL.$this->method->name.'('.$this->renderParams().') : '.$type.PHP_EOL.PHP_EOL;
                        return $out;
                }

        private function renderParams() {

                $out = [];

                foreach($this->method->getParameters() as $refParam) {

                        $aNull = ($refParam->allowsNull()) ? '?' : '';
                        $str = $aNull.$refParam->getType().' $'.$refParam->getName();

                        if ($default = $refParam->getDefaultValue()) {
                                $str.= ' = '.$default;
                        }

                        $out[] = $str;
                }

                return implode(', ', $out);
        }
}

/////////////////////////////////////////////////////////////////////////

function scanForRPCHandlers() : array {

        $out = [];

        $classes = get_declared_classes();

        foreach($classes as $class) {

                $c = new \ReflectionClass($class);

                if ($c->isSubclassOf(Handler::class)) {

                        $handlerName = $class;

                        $methods = $c->getMethods(\ReflectionMethod::IS_PUBLIC);

                        foreach($methods as $m) {

                                if ($m->name == '__construct') continue;

                                if (! isset($out[$handlerName])) {
                                        $out[$handlerName] = [];
                                }

                                $out[$handlerName][$m->name] = new RPCMethodDocumentation($handlerName, $m);
                        }
                }
        }

        return $out;
}

///////////////////////////////////////////////////////////////////////////////////////////

class ElyRPCServer extends \Xin\Swoole\Rpc\Server {

        private static $server;

        public function serve($host, $port, $config = []) {

                if (!extension_loaded('swoole')) {
                        echo Color::error('The swoole extension is not installed');
                        return;
                }

                if (!extension_loaded('signal_handler')) {
                        echo Color::error('The signal_handler extension is not installed');
                        return;
                }

                $this->host = $host;
                $this->port = intval($port);
                $this->config = $config;

                set_time_limit(0);
                $server = new \Swoole\Server($this->host, $this->port, SWOOLE_BASE);

                $server->set($config);

                $server->on('receive', [$this, 'receive']);
                $server->on('workerStart', [$this, 'workerStart']);

                $this->beforeServerStart($server);

                $server->start();
        }

        public static function shutdownServer() : void {

                global $workerPID;
                if (getmypid() === $workerPID->get()) return;

                $pidFile = '/var/run/'.me().'/'.me().'.pid';
                if (file_exists($pidFile)) @unlink($pidFile);

                // Uinregister here with WSSERVER
                /*
                go(function () {

                        try {
                                $p = (int) constant(strtoupper(me()).'_PORT');
                                [$ip, $port] = explode(':', COMMON_WSSERVER_ADDRESS);
                                $client = new HttpClient($ip, $port);
                                $client->set(['timeout' => 3]);
                                $client->upgrade('/');
                                $ipAndPort = '-'.getLocalIP().':'.$p.'='.me().'-';
                                $client->push($ipAndPort);
                                $reply  = $client->recv()->data;
                                logger(sprintf("De-registered at '%s' - Reply was: '%s' ", COMMON_WSSERVER_ADDRESS, $reply));
                                $client->close();
                        } catch(Exception) {}
                });
                 */

                $unixSocket = '/var/run/'.me().'.sock';
                if (file_exists($unixSocket)) @unlink($unixSocket);

                logger('Shutting down ...');
                closelog();
        }

        public function workerStart($server, $workerId) {

                global $workerPID;

                swoole_set_process_name(me().' [rpc] [worker]');

                $workerPID->set(getmypid());
        }

        public function receive($server, $fd, $reactor_id, $data) {

                $data = trim($data);
                if ($this->debug) {
                        echo Color::colorize("fd:{$fd} data:{$data}", Color::FG_LIGHT_GREEN) . PHP_EOL;
                }
                try {
                        $data = json_decode($data, true);
                        $service = $data[Enum::SERVICE];
                        $method = $data[Enum::METHOD];
                        $arguments = $data[Enum::ARGUMENTS];

                        if (!isset($this->services[$service])) {
                                throw new RpcException('The service handler does not exist!');
                        }

                        $ref = new ReflectionClass($this->services[$service]);
                        $handler = $ref->newInstance($server, $fd, $reactor_id);
                        $result = $handler->$method(...$arguments);
                        $response = $this->success($result);
                        $server->send($fd, json_encode($response));

                        if ($this->logger && $this->logger instanceof LoggerInterface) {
                                $this->logger->info($data, $response);
                        }
                } catch (\Exception $ex) {
                        $response = $this->fail($ex->getCode(), $ex->getMessage());
                        $server->send($fd, json_encode($response));

                        if ($this->logger && $this->logger instanceof LoggerInterface) {
                                $this->logger->error($data, $response, $ex);
                        }
                }
        }

        public function beforeServerStart($server) {

                swoole_set_process_name(me().' [rpc] [master]');

                if (posix_isatty(\STDOUT)) {

                        $me = me();
                        echo Color::colorize("-----------------------------------------------------------------------------------", Color::FG_LIGHT_GREEN) . PHP_EOL;
                        echo Color::colorize("          {$me} :: RPC Server - Listening on TCP {$this->port}        ", Color::FG_CYAN , Color::FG_LIGHT_GREEN) . PHP_EOL;
                        echo Color::colorize("-----------------------------------------------------------------------------------", Color::FG_LIGHT_GREEN) . PHP_EOL;
                }
        }
}

class RPCLogger implements LoggerInterface {

        public function info($request, $response) {

                var_dump($request, $response);

                $info = '';

                // TODO
                logger('INFO [ RPC ] :: '.$info);
        }

        public function error($request, $response, Exception $ex) {

                // TODO

        }
}

/////////////////////////////////////////////////////////////////////////////////////////

class ElyRPCHandler extends Handler {

        public function __construct($server, $fd, $reactorId) {
                $this->server = $server;
                $this->fd = $fd;
                $this->reactorId = $reactorId;
        }
}

/////////////////////////////////////////////////////////////////////////////////////////

class TestHandler extends ElyRPCHandler {

        /**
         * Returns 'success' !
         */
        public function test() : string {

                return 'success';
        }

        public function test2(array $foo = []) {

                return array_reverse($foo);
        }

        private function foobar() {

                return 'HELLO';
        }
}

/////////////////////////////////////////////////////////////////////////////////////////

$handlers = scanForRPCHandlers();

//var_dump($handlers);

foreach($handlers as $handler => $methods) {

        echo $handler.PHP_EOL;
        echo "------------------------------------".PHP_EOL;

        foreach($methods as $method) {
                echo (string) $method.PHP_EOL;
        }
}

if (posix_getuid() !== 0) {
        echo "Error: Must be started as root\n";
        die(1);
}

$daemonize = TRUE;
$rpcDebug = FALSE;
$rpcService = 'test';
$rpcServiceClass = TestHandler::class;

$server = new ElyRPCServer();
$server->logger = new \RPCLogger();
$server->setDebug($rpcDebug);
$server->setHandler($rpcService, $rpcServiceClass);

if (! $daemonize) {

        attach_signal(SIGINT, function() {

                ElyRPCServer::shutdownServer();
                sleep(1);
                posix_kill(getmypid(), SIGKILL);
        });

} else {

        attach_signal(SIGTERM, function() {

                global $workerPID;
                ElyRPCServer::shutdownServer();
                sleep(1);
                posix_kill($workerPID->get(), SIGKILL);
                posix_kill(getmypid(), SIGKILL);

        });

        attach_signal(SIGQUIT, function() {

                global $workerPID;
                ElyRPCServer::shutdownServer();
                sleep(1);
                posix_kill($workerPID->get(), SIGKILL);
                posix_kill(getmypid(), SIGKILL);
        });
}

$userInfo=posix_getpwnam(constant(strtoupper(me()).'_RUN_AS_USER'));
$userID=$userInfo["uid"];

$pidFile = '/var/run/'.me().'/'.me().'.pid';

@mkdir(dirname($pidFile), 0777);
chmod(dirname($pidFile), 0777);
chown(dirname($pidFile), $userID);
touch($pidFile);
chmod($pidFile, 0777);
chown($pidFile, $userID);

// TODO: Log file

// Become the proper user
posix_setuid($userID);

$port = (int) constant(strtoupper(me()).'_PORT');

// Start server
$server->serve('0.0.0.0', $port, [
        'log_file'          =>  $logFile,
        'pid_file'          =>      $pidFile,
        'daemonize'         =>      $daemonize,
        'max_request'                   =>      500,            // Total number of requests to be processed
        'open_eof_check'                =>      TRUE,
        'package_eof'                   =>      "\r\n",
]);