php多程式管理器

夢想星辰大海發表於2022-08-19

以下程式碼實現如下功能:

  1. 根據入參命令,啟動多個子程式
  2. 自動拉起異常退出的子程式
  3. 監聽系統結束程式訊號,並優雅給到子程式

使用示例:

php -d extension=swow index.php --command="php -d extension=swow test.php"

index.php程式碼:

<?php

//nohup php -d extension=swow index.php --command="php -d extension=swow test.php" 1>./stdout.log 2>./stderr.log &
namespace {
    require __DIR__ . DIRECTORY_SEPARATOR . 'vendor/autoload.php';

    use Guard\Parameter;
    use Guard\Coordinator;
    use Guard\Manager;
    use Swow\Coroutine;
    use Swow\Sync\WaitGroup;
    use Symfony\Component\Process\Process;

    //檢查是否輸入help提示
    if (Parameter::getHelp()) {
        Parameter::displayHelp();
        exit(0);
    }
    //初始化訊號排程器
    Coordinator::init();
    //初始化協程管理器
    $wg = new WaitGroup();
    //啟動若干個子程式
    for ($workerId = 0; $workerId < Parameter::getWorkers(); $workerId++) {
        $wg->add();
        Coroutine::run(static function () use ($workerId, $wg): void {
            try {
                $env = ['PROCESS_WORKER_ID' => $workerId, 'PROCESS_WORKERS' => Parameter::getWorkers()];
                restart:
                $process = new Process(Parameter::getCommand(), null, $env);
                $process->setTimeout(null);
                $process->start(function ($type, $buffer) {
                    if (substr($buffer, 0 - strlen(PHP_EOL)) != PHP_EOL) {
                        $buffer .= PHP_EOL;
                    }
                    if (Process::ERR === $type) {
                        fwrite(Parameter::getLogfileStderr(), $buffer);
                    } else {
                        fwrite(Parameter::getLogfileStdout(), $buffer);
                    }
                });
                Manager::set($workerId, $process);
                $process->wait();
                Manager::del($workerId);
                sleep(1);
                if (Parameter::getAutoRestart() && Coordinator::$running) {
                    goto restart;
                }
            } catch (Throwable) {
            } finally {
                $wg->done();
            }
        });
    }
    //定時檢查是否有程式停止訊號
    $wg->add();
    Coroutine::run(static function () use ($wg): void {
        try {
            while (Coordinator::$running) {
                Coordinator::yield(3 * 1000);
            }
            Manager::stop();
        } catch (Throwable) {
        } finally {
            $wg->done();
        }
    });
    $wg->wait();
}

/*---------------------------------------------------分割線------------------------------------------------------------*/

namespace Guard {

    use Swow\Channel;
    use Swow\ChannelException;
    use Swow\Coroutine;
    use Swow\Errno;
    use Swow\Signal;
    use Swow\SignalException;
    use Symfony\Component\Process\Process;
    use Throwable;
    use InvalidArgumentException;
    use RuntimeException;

    /**
     * 訊號排程類
     * Class Coordinator
     */
    class Coordinator
    {
        /**
         * @var bool
         */
        public static bool $running = true;
        protected static Channel $ch;
        protected static bool $initLock = false;

        /**
         * @param int $timeout 單位毫秒,-1表示不超時
         * @return bool
         */
        public static function yield(int $timeout): bool
        {
            try {
                self::$ch->pop($timeout);
                return !self::$ch->isAvailable();
            } catch (ChannelException $exception) {
                $code = $exception->getCode();
                if ($code === Errno::ETIMEDOUT) {
                    return false;
                }
                if ($code === Errno::ECANCELED) {
                    return true;
                }
                //Channel is closing
                if ($code === Errno::ECLOSING) {
                    return true;
                }
                //Channel has been closed
                if ($code === Errno::ECLOSED) {
                    return true;
                }
                fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
                return true;
            } catch (Throwable $throwable) {
                fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
                return true;
            }
        }

        /**
         * @param int $timeout 單位:毫秒
         */
        public static function init(int $timeout = 60 * 1000)
        {
            if (self::$initLock) {
                return;
            }
            self::$initLock = true;
            self::$ch = new Channel();
            if ($timeout < 5 * 1000) {
                $timeout = 5 * 1000;
            }
            if ('\\' === DIRECTORY_SEPARATOR) {
                //windows下只監聽Ctrl+c
                $signals = [Signal::INT];
            } else {
                //linux下監聽Ctrl+c和SIGTERM
                $signals = [Signal::TERM, Signal::INT, Signal::HUP];
            }
            foreach ($signals as $signal) {
                Coroutine::run(static function () use ($timeout, $signal): void {
                    while (true) {
                        try {
                            Signal::wait($signal, $timeout);
                            if ($signal == Signal::HUP) {
                                //忽略關閉session傳送的SIGHUP訊號
                                continue;
                            }
                            self::$running = false;
                            if (self::$ch->isAvailable()) {
                                try {
                                    self::$ch->close();
                                } catch (Throwable) {
                                }
                            }
                            break;
                        } catch (SignalException $exception) {
                            if ($exception->getCode() !== Errno::ETIMEDOUT) {
                                fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
                            }
                            continue;
                        } catch (Throwable $throwable) {
                            fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
                        }
                    }
                });
            }
        }

        protected static function formatThrowable(Throwable $throwable): string
        {
            $message = $throwable->getMessage();
            $message = trim($message);
            if (strlen($message) == 0) {
                $message = get_class($throwable);
            }
            return sprintf(
                "%d --> %s in %s on line %d\nThrowable: %s\nStack trace:\n%s",
                $throwable->getCode(),
                $message,
                $throwable->getFile(),
                $throwable->getLine(),
                get_class($throwable),
                $throwable->getTraceAsString()
            );
        }
    }

    /**
     * 子程式容器
     * Class Manager
     */
    class Manager
    {
        /**
         * @var array | Process[]
         */
        protected static array $container = [];

        public static function set(int $workerId, Process $process)
        {
            self::$container[$workerId] = $process;
        }

        public static function del(int $workerId)
        {
            unset(self::$container[$workerId]);
        }

        public static function stop()
        {
            //給子程式傳送訊號
            foreach (self::$container as $process) {
                if ($process->isRunning()) {
                    $process->signal(Signal::TERM);
                }
            }
            //等待子程式結束
            $timeout = Parameter::getStopTimeout();
            while ($timeout > 0) {
                $stop = 0;
                foreach (self::$container as $process) {
                    if (!$process->isRunning()) {
                        $stop++;
                    }
                }
                if ($stop == count(self::$container)) {
                    break;
                }
                $timeout--;
                sleep(1);
            }
            //超時時間到,強殺未結束的子程式
            foreach (self::$container as $process) {
                if ($process->isRunning()) {
                    $process->signal(Signal::KILL);
                }
            }
        }
    }

    /**
     * 外部引數獲取類
     * Class Parameter
     */
    class Parameter
    {
        protected static function argv($key): ?string
        {
            global $argv;
            foreach ($argv as $v) {
                if (stripos($v, $key) === 0) {
                    return trim(substr($v, strlen($key) + 1), '"');
                }
            }
            return null;
        }

        protected const OPTIONS_HELP = '--help';
        protected const OPTIONS_WORKERS = '--workers';
        protected const OPTIONS_LOGFILE_STDERR = '--logfile_stderr';
        protected const OPTIONS_LOGFILE_STDOUT = '--logfile_stdout';
        protected const OPTIONS_COMMAND = '--command';
        protected const OPTIONS_STOP_TIMEOUT = '--stop_timeout';
        protected const OPTIONS_AUTO_RESTART = '--auto_restart';

        public static function displayHelp()
        {
            $s = ['Options:'];
            $space = strlen(self::OPTIONS_LOGFILE_STDERR) + 3;
            $format = "%-{$space}s";
            $s[] = sprintf($format, self::OPTIONS_HELP) . "Display help.";
            $s[] = sprintf($format, self::OPTIONS_COMMAND) . "Command to execute";
            $s[] = sprintf($format, self::OPTIONS_WORKERS) . "Number of worker processes to start, default --> " . self::getWorkers();
            $s[] = sprintf($format, self::OPTIONS_LOGFILE_STDERR) . "Output STDERR file, default --> php://stderr";
            $s[] = sprintf($format, self::OPTIONS_LOGFILE_STDOUT) . "Output STDOUT file, default --> php://stdout";
            $s[] = sprintf($format, self::OPTIONS_STOP_TIMEOUT) . "Timeout to wait for the process to end, default --> " . self::getStopTimeout();
            $s[] = sprintf($format, self::OPTIONS_AUTO_RESTART) . "Auto restart the closed process, default --> " . (self::getAutoRestart() ? 'true' : 'false');
            echo implode(PHP_EOL, $s), PHP_EOL;
        }

        public static function getHelp(): bool
        {
            global $argv;
            if (count($argv) == 1) {
                return true;
            }
            return !is_null(self::argv(self::OPTIONS_HELP));
        }

        protected static int $workers = 0;

        public static function getWorkers(): int
        {
            if (self::$workers > 0) {
                return self::$workers;
            }
            self::$workers = (int)self::argv(self::OPTIONS_WORKERS);
            if (self::$workers <= 0) {
                $tmp = @file_get_contents('/proc/cpuinfo');
                self::$workers = substr_count((string)$tmp, 'processor');
            }
            if (self::$workers <= 0) {
                self::$workers = 2;
            }
            return self::$workers;
        }

        protected static $logfileStderr;

        public static function getLogfileStderr()
        {
            if (is_resource(self::$logfileStderr)) {
                return self::$logfileStderr;
            }
            $logfileStderr = (string)self::argv(self::OPTIONS_LOGFILE_STDERR);
            if ($logfileStderr == '') {
                self::$logfileStderr = STDERR;
                return self::$logfileStderr;
            }
            if (!is_dir(dirname($logfileStderr))) {
                throw new InvalidArgumentException("Cannot found directory $logfileStderr");
            }
            $tmp = fopen($logfileStderr, 'a+');
            if (is_resource(!$tmp)) {
                throw new RuntimeException("Cannot open file $logfileStderr");
            }
            self::$logfileStderr = $tmp;
            return self::$logfileStderr;
        }

        protected static $logfileStdout;

        public static function getLogfileStdout()
        {
            if (is_resource(self::$logfileStdout)) {
                return self::$logfileStdout;
            }
            $logfileStdout = (string)self::argv(self::OPTIONS_LOGFILE_STDOUT);
            if ($logfileStdout == '') {
                self::$logfileStdout = STDOUT;
                return self::$logfileStdout;
            }
            if (!is_dir(dirname($logfileStdout))) {
                throw new InvalidArgumentException("Cannot found directory $logfileStdout");
            }
            $tmp = fopen($logfileStdout, 'a+');
            if (is_resource(!$tmp)) {
                throw new RuntimeException("Cannot open file $logfileStdout");
            }
            self::$logfileStdout = $tmp;
            return self::$logfileStdout;
        }

        protected static array $command = [];

        public static function getCommand(): array
        {
            if (count(self::$command) != 0) {
                return self::$command;
            }
            $command = self::argv(self::OPTIONS_COMMAND);
            foreach ((array)explode(" ", $command) as $item) {
                $item = trim($item);
                if ($item == '') {
                    continue;
                }
                self::$command[] = $item;
            }
            if (count(self::$command) == 0) {
                throw new InvalidArgumentException('Please enter the command: ' . self::$logfileStdout);
            }
            return self::$command;
        }

        protected static int $stopTimeout = 0;

        public static function getStopTimeout(): int
        {
            if (self::$stopTimeout > 0) {
                return self::$stopTimeout;
            }
            self::$stopTimeout = (int)self::argv(self::OPTIONS_STOP_TIMEOUT);
            if (self::$stopTimeout <= 0) {
                self::$stopTimeout = 10;
            }
            return self::$stopTimeout;
        }

        protected static int $autoRestart = 0;

        public static function getAutoRestart(): bool
        {
            if (self::$autoRestart == 0) {
                $tmp = self::argv('--auto_restart');
                if (is_null($tmp)) {
                    self::$autoRestart = 1;
                } else {
                    self::$autoRestart = $tmp === 'false' ? 0 : 1;
                }
            }
            return (bool)self::$autoRestart;
        }
    }
}

test.php程式碼:

<?php

use Guard\Parameter;
use Swow\Channel;
use Swow\ChannelException;
use Swow\Coroutine;
use Swow\Errno;
use Swow\Signal;
use Swow\SignalException;

require __DIR__ . DIRECTORY_SEPARATOR . 'vendor/autoload.php';

Coordinator::init();

$process_worker_id = getenv('PROCESS_WORKER_ID');
try {
    $i = 0;
    while (Coordinator::$running) {
        if ($i == 3 && $process_worker_id == 0) {
            throw new RuntimeException('執行異常');
        }
        $i++;
        echo $process_worker_id . ' ' . date('Y-m-d H:i:s').PHP_EOL;
        Coordinator::yield(3000);
    }
    echo $process_worker_id . ' ' . '收到程式結束訊號' . PHP_EOL;
    sleep(1);
    echo $process_worker_id . ' ' . '結束程式' . PHP_EOL;
}catch (RuntimeException $exception) {
    fwrite(STDERR, $exception->getMessage());
}

class Coordinator
{
    /**
     * @var bool
     */
    public static bool $running = true;
    protected static Channel $ch;
    protected static bool $initLock = false;

    /**
     * @param int $timeout 單位毫秒,-1表示不超時
     * @return bool
     */
    public static function yield(int $timeout): bool
    {
        try {
            self::$ch->pop($timeout);
            return !self::$ch->isAvailable();
        } catch (ChannelException $exception) {
            $code = $exception->getCode();
            if ($code === Errno::ETIMEDOUT) {
                return false;
            }
            if ($code === Errno::ECANCELED) {
                return true;
            }
            //Channel is closing
            if ($code === Errno::ECLOSING) {
                return true;
            }
            //Channel has been closed
            if ($code === Errno::ECLOSED) {
                return true;
            }
            fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
            return true;
        } catch (Throwable $throwable) {
            fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
            return true;
        }
    }

    /**
     * @param int $timeout 單位:毫秒
     */
    public static function init(int $timeout = 60 * 1000)
    {
        if (self::$initLock) {
            return;
        }
        self::$initLock = true;
        self::$ch = new Channel();
        if ($timeout < 5 * 1000) {
            $timeout = 5 * 1000;
        }
        if ('\\' === DIRECTORY_SEPARATOR) {
            //windows下只監聽Ctrl+c
            $signals = [Signal::INT];
        } else {
            //linux下監聽Ctrl+c和SIGTERM
            $signals = [Signal::TERM, Signal::INT, Signal::HUP];
        }
        foreach ($signals as $signal) {
            Coroutine::run(static function () use ($timeout, $signal): void {
                while (true) {
                    try {
                        Signal::wait($signal, $timeout);
                        if ($signal == Signal::HUP) {
                            //任何時候都忽略結束通話訊號
                            continue;
                        }
                        self::$running = false;
                        if (self::$ch->isAvailable()) {
                            try {
                                self::$ch->close();
                            } catch (Throwable) {
                            }
                        }
                        break;
                    } catch (SignalException $exception) {
                        if ($exception->getCode() !== Errno::ETIMEDOUT) {
                            fwrite(Parameter::getLogfileStderr(), self::formatThrowable($exception));
                        }
                        continue;
                    } catch (Throwable $throwable) {
                        fwrite(Parameter::getLogfileStderr(), self::formatThrowable($throwable));
                    }
                }
            });
        }
    }

    protected static function formatThrowable(Throwable $throwable): string
    {
        $message = $throwable->getMessage();
        $message = trim($message);
        if (strlen($message) == 0) {
            $message = get_class($throwable);
        }
        return sprintf(
            "%d --> %s in %s on line %d\nThrowable: %s\nStack trace:\n%s",
            $throwable->getCode(),
            $message,
            $throwable->getFile(),
            $throwable->getLine(),
            get_class($throwable),
            $throwable->getTraceAsString()
        );
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章