Using swoole to implement process daemon

In the previous article, "Using swoole to implement the daemon of processes (2)", we implemented a Daemon class that can simultaneously guard multiple scripts by reading configuration.
This article attempts to continue extending the Daemon class so that it can overload the configuration without restarting the process.
The most common way of hot overload is to send system signals to the process. When the process listens to the corresponding signals, it can simply reload the memory configurated to the process space.
High performance permanent process servers such as Nginx and Addy are also used to achieve hot overload in order to avoid server unavailability caused by restarting processes.

bash in Linux can view all supported process signals through kill-l command

 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

We can do this by selecting SIGUSR1 to listen to user-defined signals.

PHP officially provides two functions to handle process numbers, namely:

  1. Pcntl_signal (SIGINT,'signalHandler'); used to register the processing function after receiving the signal.
  2. pcntl_signal_dispatch() is used to call the processor registered by pcntl_signal() for each waiting signal.

Then, the sample code of the registered signal processor can be similar to the following:

pcntl_signal(SIGHUP, function () {
    printf("Receiving overload configuration signal\n");
    $this->loadWorkers();
    printf("Overload Configuration Completed\n");
});

The scheduled signal processor can execute each time the process is reclaimed:

while (1) {
    pcntl_signal_dispatch();
    if ($ret = Process::wait(false)) {
        // todo something
    }
}

Thus, the Daemon class can be extended as follows:

namespace App;

use Swoole\Process;

class Daemon
{
    /**
     * @var string
     */
    private $configPath;

    /**
     * @var Command[]
     */
    private $commands;

    /**
     * @var Worker[]
     */
    private $workers = [];

    public function __construct(string $configPath)
    {
        $this->configPath = $configPath;
    }

    public function run()
    {
        $this->loadWorkers();

        pcntl_signal(SIGHUP, function () {
            printf("Receiving overload configuration signal\n");
            $this->loadWorkers();
            printf("Overload Configuration Completed\n");
        });

        $this->waitAndRestart();
    }

    /**
     * Recovery process and restart
     */
    private function waitAndRestart()
    {
        while (1) {
            pcntl_signal_dispatch();
            if ($ret = Process::wait(false)) {

                $retPid = intval($ret["pid"] ?? 0);
                $index = $this->getIndexOfWorkerByPid($retPid);

                if (false !== $index) {
                    if ($this->workers[$index]->isStopping()) {
                        printf("[%s] Remove guardianship %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());

                        unset($this->workers[$index]);
                    } else {
                        $command = $this->workers[$index]->getCommand()->getCommand();
                        $newPid = $this->createWorker($command);
                        $this->workers[$index]->setPid($newPid);

                        printf("[%s] Pull it up again %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());
                    }
                }

            }
        }
    }


    /**
     * Load workers
     */
    private function loadWorkers()
    {
        $this->parseConfig();
        foreach ($this->commands as $command) {
            if ($command->isEnabled()) {
                printf("[%s] Enable %s\n", date("Y-m-d H:i:s"), $command->getId());
                $this->startWorker($command);
            } else {
                printf("[%s] Discontinue use %s\n", date("Y-m-d H:i:s"), $command->getId());
                $this->stopWorker($command);
            }
        }
    }

    /**
     * Start worker
     * @param Command $command
     */
    private function startWorker(Command $command)
    {
        $index = $this->getIndexOfWorker($command->getId());
        if (false === $index) {
            $pid = $this->createWorker($command->getCommand());

            $worker = new Worker();
            $worker->setPid($pid);
            $worker->setCommand($command);
            $this->workers[] = $worker;
        }
    }

    /**
     * Stop worker
     * @param Command $command
     */
    private function stopWorker(Command $command)
    {
        $index = $this->getIndexOfWorker($command->getId());
        if (false !== $index) {
            $this->workers[$index]->setStopping(true);
        }
    }

    /**
     *
     * @param $commandId
     * @return bool|int|string
     */
    private function getIndexOfWorker(string $commandId)
    {
        foreach ($this->workers as $index => $worker) {
            if ($commandId == $worker->getCommand()->getId()) {
                return $index;
            }
        }
        return false;
    }

    /**
     * @param $pid
     * @return bool|int|string
     */
    private function getIndexOfWorkerByPid($pid)
    {
        foreach ($this->workers as $index => $worker) {
            if ($pid == $worker->getPid()) {
                return $index;
            }
        }
        return false;
    }

    /**
     * Parsing configuration files
     */
    private function parseConfig()
    {
        if (is_readable($this->configPath)) {
            $iniConfig = parse_ini_file($this->configPath, true);

            $this->commands = [];
            foreach ($iniConfig as $id => $item) {
                $commandLine = strval($item["command"] ?? "");
                $enabled = boolval($item["enabled"] ?? false);

                $command = new Command();
                $command->setId($id);
                $command->setCommand($commandLine);
                $command->setEnabled($enabled);
                $this->commands[] = $command;
            }
        }
    }

    /**
     * Create a child process and return the child process id
     * @param $command
     * @return int
     */
    private function createWorker(string $command): int
    {
        $process = new Process(function (Process $worker) use ($command) {
            $worker->exec('/bin/sh', ['-c', $command]);
        });
        return $process->start();
    }

}

Note: For the sake of code simplicity, the above code adds a Worker class as follows:

class Worker
{
    /**
     * @var Command
     */
    private $command;

    /**
     * @var int
     */
    private $pid;

    /**
     * @var bool
     */
    private $stopping;

    // ... The Get Set method is omitted below
}

Finally, the use of this Daemon class remains as follows:

$pid = posix_getpid();
printf("Main process number: {$pid}\n");

$configPath = dirname(__DIR__) . "/config/daemon.ini";

$daemonMany = new Daemon($configPath);
$daemonMany->run();

Then, if we know that the Daemon program is running with a process number of 522, we can achieve the configuration hot overload by following commands:

kill -USR1 522

So far, the Daemon class is fully functional, but there is still room for improvement.
For example, is there a way to automatically apply the latest configuration by the program itself without the need for users to send signals to the process manually to overload the configuration?

The next article uses swoole to implement the daemon of processes (4) to try to extend the Daemon class with swoole's protocols.

Tags: PHP Nginx Linux

Posted on Sun, 01 Sep 2019 08:14:29 -0700 by hypedupdawg