PHP process manager

image

Всем привет!

На Хабре было много статей, о том как писать демоны на PHP и другие fork-нутые вещи. Хочу поделится с вами своими наработками на схожую, но все-таки несколько другую тему — управление несколькими PHP процессами.


Для начала небольшой словарь терминов, используемых в статье.
  • Job (работа) — задача, выполняемая в отдельном процессе. Наберите в консоли «php test.php» — вот вам job.
  • Job Manager или Process Manager — процесс, управляющий задачами. Собирает и обрабатывает их вывод и может посылать сообщения на ввод.


Цель поставленной задачи в том, чтобы иметь возможность влиять на уже запущенные и работающие процессы и получать информацию о ходе их выполнения.

Для запуска новых процессов я использую функцию proc_open, которая позволяет переопределять дескрипторы ввода/вывода для нового процесса. Для управления отдельным процессом был разработан класс Job. Работа характеризуется названием и выполняемой командой.

class Job {
    protected $_pid = 0;
    protected $_name;

    protected $_cmd = '';

    protected $_stderr = '/dev/null';

    private $_resource = NULL;
    private $_pipes = array();
    private $_waitpid = TRUE;

    public function __construct($cmd, $name = 'job') {
        $this->_cmd = $cmd;
        $this->_name = $name;
    }

    public function __destruct() {

        // ожидаем завершения процесса

        if ($this->_resource) {
            if ($this->_waitpid && $this->isRunning()) {
                echo "Waiting for job to complete ";

                $status = NULL;
                pcntl_waitpid($this->_pid, $status);
                
                /*while ($this->isRunning()) {
                    echo '.';
                    sleep(1);
                }*/
                echo "\n";
            }
        }

        // закрываем дескрипторы

        if (isset($this->_pipes) && is_array($this->_pipes)) {
            foreach (array_keys($this->_pipes) as $index ) {
                if (is_resource($this->_pipes[$index])) {
                    fflush($this->_pipes[$index]);
                    fclose($this->_pipes[$index]);
                    unset($this->_pipes[$index]);
                }
            }
        }

        // закрываем открытый хэндлер

        if ($this->_resource) {
            proc_close($this->_resource);
            unset($this->_resource);
        }
       
    }

    public function pid() {
        return $this->_pid;
    }

    public function name() {
        return $this->_name;
    }

    // функция чтения из "трубы". $nohup отвечает за блокирование при чтении
    private function readPipe($index, $nohup = FALSE) {
        if (!isset($this->_pipes[$index])) return FALSE;

        if (!is_resource($this->_pipes[$index]) || feof($this->_pipes[$index])) return FALSE;

        if ($nohup) {
            $data = '';
            while ($line = fgets($this->_pipes[$index])) {
                $data .= $line;
            }
            
            return $data;
        }

        while ($data = fgets($this->_pipes[$index])) {
            echo $data;
        }
    }

    public function pipeline($nohup = FALSE) {
        return $this->readPipe(1, $nohup);
    }

    public function stderr($nohup = FALSE) {
        return $this->readPipe(2, $nohup);
    }

    // запуск задачи в новом процессе
    public function execute() {
                // определяем откуда будет читать и куда писать процесс
        $descriptorspec = array(
            0 => array('pipe', 'r'),  // stdin
            1 => array('pipe', 'w'),  // stdout
            2 => array('pipe', 'w') // stderr 
        );


        $this->_resource = proc_open('exec '.$this->_cmd, $descriptorspec, $this->_pipes);

        // ставим неблокирующий режим всем дескрипторам
        stream_set_blocking($this->_pipes[0], 0); 
        stream_set_blocking($this->_pipes[1], 0);
        stream_set_blocking($this->_pipes[2], 0);

        if (!is_resource($this->_resource)) return FALSE;

        $proc_status     = proc_get_status($this->_resource);
        $this->_pid      = isset($proc_status['pid']) ? $proc_status['pid'] : 0;
    }

    public function getPipe() {
        return $this->_pipes[1];
    }

    public function getStderr() {
        return $this->_pipes[2];
    }

    public function isRunning() {
        if (!is_resource($this->_resource)) return FALSE;

        $proc_status = proc_get_status($this->_resource);
        return isset($proc_status['running']) && $proc_status['running'];
    }

    // посылка сигнала процессу
    public function signal($sig) {
        if (!$this->isRunning()) return FALSE;

        posix_kill($this->_pid, $sig);
    }

    // отправка сообщения в STDIN процесса
    public function message($msg) {
        if (!$this->isRunning()) return FALSE;

        fwrite($this->_pipes[0], $msg);     
    }
}


Для управления работами создан класс Job_Manager, который по сути является ключевым во всей схеме.

class Job_Manager {
    private $_pool_size = 20;
    private $_pool = array();
    private $_streams = array();
    private $_stderr = array();

    private $_is_terminated = FALSE;
    protected $_dispatch_function = NULL;

    public function __construct() {
        // init pool
        // 
    }

    public function __destruct() {
        // destroy pool
        foreach (array_keys($this->_pool) as $index) {
            $this->stopJob($index);
        }
    }

    // Проверяем статус запущенных задач
    private function checkJobs() {
        $running_jobs = 0;
        foreach ($this->_pool as $index => $job) {
            if (!$job->isRunning()) {
                echo "Stopping job ".$this->_pool[$index]->name()." ($index)" . PHP_EOL;
                $this->stopJob($index);
            } else {
                $running_jobs++;
            }
        }

        return $running_jobs;
    }

    private function getFreeIndex() {
        foreach ($this->_pool as $index => $job) {
            if (!isset($job)) return $index;
        }

        return count($this->_pool) < $this->_pool_size ? count($this->_pool) : -1;
    }

    // Запуск новой задачи
    public function startJob($cmd, $name = 'job') {
        // broadcast existing jobs
        $this->checkJobs();

        $free_pool_slots = $this->_pool_size - count($this->_pool);

        if ($free_pool_slots <= 0) {
            // output error "no free slots in the pool"
            return -1;
        }

        $free_slot_index = $this->getFreeIndex();
        if ($free_slot_index < 0) {
            return -1;
        }

        echo "Starting job $name ($free_slot_index)" . PHP_EOL;
        $this->_pool[$free_slot_index] = new Job($cmd, $name);
        $this->_pool[$free_slot_index]->execute();
        $this->_streams[$free_slot_index] = $this->_pool[$free_slot_index]->getPipe();
        $this->_stderr[$free_slot_index] = $this->_pool[$free_slot_index]->getStderr();

        return $free_slot_index;
    }

    public function stopJob($index) {
        if (!isset($this->_pool[$index]))
            return FALSE;
        
        unset($this->_streams[$index]);
        unset($this->_stderr[$index]);
        unset($this->_pool[$index]);
    }

    public function name($index) {
        if (!isset($this->_pool[$index]))
            return FALSE;

        return $this->_pool[$index]->name();
    }

    public function pipeline($index, $nohup = FALSE) {
        if (!isset($this->_pool[$index]))
            return FALSE;

        return $this->_pool[$index]->pipeline($nohup);
    }   

    public function stderr($index, $nohup = FALSE) {
        if (!isset($this->_pool[$index]))
            return FALSE;

        return $this->_pool[$index]->stderr($nohup);
    }

    private function broadcastMessage($msg) {
        // sends selected signal to all child processes
        foreach ($this->_pool as $pool_index => $job) {
            $job->message($msg);
        }
    }

    private function broadcastSignal($sig) {
        // sends selected signal to all child processes
        foreach ($this->_pool as $pool_index => $job) {
            $job->signal($sig);
        }
    }

    // если была зарегистрирована пользовательская функция разбора - используем ее
    protected function dispatch($cmd) {
        if (is_callable($this->_dispatch_function)) {
            call_user_func($this->_dispatch_function, $cmd);
        }
    }

    // регистрация пользовательской функции для разбора
    public function registerDispatch($callable) {
        if (is_callable($callable)) {
            $this->_dispatch_function = $callable;
        } else {
            trigger_error("$callable is not callable func", E_USER_WARNING);
        }
    }

    // разбираем пользовательский ввод
    private function dispatchMain($cmd) {
        $parts = explode(' ', $cmd);
        $arg = isset($parts[0]) ? $parts[0] : '';
        $val = isset($parts[1]) ? $parts[1] : '';
        switch ($arg) {
            case "exit": 
                $this->broadcastSignal(SIGTERM);
                $this->_is_terminated = TRUE;
                break;

            case "test":
                echo 'sending test' . PHP_EOL;
                $this->broadcastMessage('test');
                $this->broadcastSignal(SIGUSR1);
                break;
            case 'kill':
                $pool_index = $val !== '' && (int)$val >= 0 ? (int)$val : -1;
                if ($pool_index >= 0 && isset($this->_pool[$pool_index])) {
                    $this->_pool[$pool_index]->signal(SIGKILL);
                }
                break;
            default:
                $this->dispatch($cmd);
                break;
        }
        return FALSE;
    }

    public function process() {
        stream_set_blocking(STDIN, 0);

        $write = NULL;
        $except = NULL;
        while (!$this->_is_terminated) {
            /*
            из-за особенности функции stream_select приходится особым образом работать с массивами дескрипторов
            */
            $read = $this->_streams;
            $except = $this->_stderr;
            $read[$this->_pool_size] = STDIN;

            if (is_array($read) && count($read) > 0) {
                if (false === ($num_changed_streams = stream_select($read, $write, $except, 2))) {
                    // oops
                } elseif ($num_changed_streams > 0) {
                    // есть что почитать

                    if (is_array($read) && count($read) > 0) {
                        $cmp_array = $this->_streams;
                        $cmp_array[$this->_pool_size] = STDIN;
                        foreach ($read as $resource) {
                            $pool_index = array_search($resource, $cmp_array, TRUE);
                            if ($pool_index === FALSE) continue;
                            
                            if ($pool_index == $this->_pool_size) {
                                // stdin
                                $content = '';
                                while ($cmd = fgets(STDIN)) {
                                    if (!$cmd) break;
                                    $content .= $cmd;
                                }
                                $content = trim($content);
                                if ($content) {
                                    // если Process Manager словил на вход какую-то строчку - парсим и решаем что делать
                                    $this->dispatchMain($content);
                                }
                                //echo "stdin> " . $cmd;
                            } else {
                                // читаем сообщения процессов
                                $pool_content = $this->pipeline($pool_index, TRUE);
                                $job_name = $this->name($pool_index);

                                if ($pool_content) {
                                    echo $job_name ." ($pool_index)" . ': ' . $pool_content;
                                }

                                $pool_content = $this->stderr($pool_index, TRUE);
                                if ($pool_content) {
                                    echo $job_name ." ($pool_index)" . ' [STDERR]: ' . $pool_content;
                                }
                            }
                        }
                    }
                }
            }
            $this->checkJobs();
        }
    }

}


Управлять некоторыми абстрактными задачами мы уже научились, осталось реализовать класс для самих исполняемых процессов.

class Executable {
    protected $_is_terminated = FALSE;

    protected $_cleanup_function = NULL;

    public function __construct() {
        // выставляем обработчик сигналов
        pcntl_signal(SIGTERM, array('Executable', 'signalHandler'));
        pcntl_signal(SIGHUP, array('Executable', 'signalHandler'));
        pcntl_signal(SIGINT, array('Executable', 'signalHandler'));
        pcntl_signal(SIGUSR1, array('Executable', 'signalHandler'));
        pcntl_signal(SIGUSR2, array('Executable', 'signalHandler'));

        stream_set_blocking(STDIN, 0);
        stream_set_blocking(STDOUT, 0);
        stream_set_blocking(STDERR, 0);
    }

    public function __destruct() {
        //echo "destructor called in " . get_class($this) . PHP_EOL;
        if (!$this->_is_terminated) {
            $this->_is_terminated = TRUE;
            $this->isTerminated();
        }
    }


    // финальные обработчики - если пользователь пожелает
    private function cleanup() {
        if (is_callable($this->_cleanup_function)) {
            call_user_func($this->_cleanup_function);
        }
    }

    protected function registerCleanup($callable) {
        if (is_callable($callable)) {
            $this->_cleanup_function = $callable;
        } else {
            trigger_error("$callable is not callable func", E_USER_WARNING);
        }
    }

    protected function isTerminated() {
        pcntl_signal_dispatch();
        if ($this->_is_terminated) {
            $this->cleanup();
        }

        return $this->_is_terminated;
    }

    protected function dispatch($cmd) {
        // можно смело парсить входные данные
        /*
        switch ($cmd) {

        }
        */
    }

    protected function checkStdin() {
        $read = array(STDIN);
        $write = NULL;
        $except = NULL;

        if (is_array($read) && count($read) > 0) {
            if (false === ($num_changed_streams = stream_select($read, $write, $except, 2))) {
                // oops
            } elseif ($num_changed_streams > 0) {
                if (is_array($read) && count($read) > 0) {
                    // stdin
                    $content = '';
                    while ($cmd = fgets(STDIN)) {
                        if (!$cmd) break;
                        $content .= $cmd;
                    }
                    $this->dispatch($content);
                    echo "recieved $content";
                    //echo "stdin> " . $cmd;
                }
            }
        }

    }

    // Обработчик сигналов
    protected function signalHandler ($signo) {
        switch ($signo) {
            case SIGTERM:
            case SIGHUP:
            case SIGINT:
                $this->_is_terminated = TRUE;
                //echo "exiting in ".get_class($this)."...\n";
                break;
            case SIGUSR1:
                //echo "SIGUSR1 recieved\n";
                $this->checkStdin();
                break;
            case SIGUSR2:
                $this->_is_terminated = TRUE;
                echo "[SHUTDOWN] in " . get_class($this) . PHP_EOL;
                flush();
                exit(1);
                break;
            default:
                // handle all other signals
                break;
        }
    }
}



В качестве примера использования менеджера процессов реализуем «спящий» процесс — скрипт, который будет спать и отписываться по этому поводу в STDOUT

sleep.php
class SleeperTest extends Executable {

    public function sleep() {
        for($i = 0; !$this->isTerminated() && $i < 10; $i++) {
            ob_start();
            echo $i . "\n";
            ob_end_flush();
            sleep(5);
        }
    }
}


$s = new SleeperTest;
$s->sleep();


pm.php
$pm = new Job_Manager;

$pm->startJob('php sleep.php', 'sleeper1');
$pm->startJob('php sleep.php', 'sleeper2');

// 
$pm->process();


Используемые в реализации неблокирующие дескрипторы и функция stream_select позволяют избегать проблемы, типичной для разного рода демонов — высокая загрузка ЦПУ в холостом цикле. Предложенный метод лишен этого недостатка, все работает гладко и спокойно.

UPDATE. Выложил исходники классов на github https://github.com/xzag/php-pm
Share post

Similar posts

Comments 53

    +1
    А есть то же самое, но на каком-нить гитхабе?
      +1
      вечером могу выложить
        +1
        пришлось немного потупить — никогда не работал с github, но вроде разобрался. Вот github.com/xzag/php-pm
      –12
      Я смотрю, тут всё упражняются с костылями :)
        +1
        Без иронии, я интересовался «зрелыми» и «некостыльными» решениями по этой теме, но, к сожалению, пока не нашел того, что бы меня полностью устраивало [это, пока еще не проверял]. Если у вас есть ссылки на хорошие решения — поделитесь. Или, еще лучше, воспользуйтесь recovery и напишите развернутый топик.
          –4
          О чем писать развернутый топик, если php не предназначен для этой цели?
          • UFO just landed and posted this here
              0
              Хм… ну давайте я попробую предложить ситуацию, где это нужно. Почти из жизни.
              Есть сайт и есть не совсем тривиальные модели, которые уже реализованы в php.
              И есть асинхронные задачи, требующие эти модели, например, банальная перекодировка/создание превьюшек для загруженных данных.
              На текущий момент самым практичным мне видется формирование демонизированных воркеров на php с каким-нибудь gearman'ом во главе. Ваши предложения?
                –5
                Переписать часть бэкенда на что-то более подходящее для этой цели. python, например. Я не являюсь адептом какого-либо из языков, но у любого инструмента есть своя область применения. Понятно, что иногда в уже сложившихся условиях приходится искать компромиссное решение, но в большинстве случаев «лучше день потерять, потом за 5 минут долететь».
                  +2
                  Т.е. вы предлагаете (возьмем python)
                  а) переписать требуемые модели на python, после чего стараться не забыть обновить их в случае обновления на сайте и поддерживать две ветки одновременно
                  б) переписать сайты на python, переквалифицировать/уволить имеющихся программистов и жить счастливо с более лучшим языком (объяснив по ходу инвестором, чем это мы все это время занимаемся) потому, что «в кузнице не было гвоздя»
                  Ну, конечно, можно пойти и таким путем, но я пока поищу решения подешевле.
                    +2
                    Я предлагаю вариант а), но немного не так его себе вижу. При наличии некого интерфейса обмена данными переписывать/обновлять бэкэнд придется только в том случае, когда меняется сам интерфейс (например, расширяется или оптимизируется). Ну а изменения в интерфейсе будут настолько очевидны, что забыть про бэкэнд не выйдет. У моего предыдущего работодателя фронтенд был на php, бэкэнд по большей части на perl. Когда в php упирались в определенные ограничения, то product owner'у объяснялось, что есть два пути развития костыльный и потенциально тупиковый (но относительно быстрый) и «прямой» (но относительно долгий). Когда у владельца продукта есть возможность оценить все «за» и «против», он с большой вероятностью согласится на отдельный бэкэнд вместо костылей.

                    Вариант б) я не предлагаю, потому что мало какой инвестор/продактоунер согласится на него (я бы тоже не согласился :)). Правда, в реальном мире я знаю как минимум один пример применения подхода б).

                    p.s. У минусаторов изначального коммента слишком много серьезности в голове. Смысл коммента сводился к тому, что таких вещей надо по возможности избегать.
                      0
                      Я рад, что в треде появилось немного конструктивизма.

                      К сожалению, в моих реалиях есть китайцы, общением с которыми и занимаются те самые модели (построенные, надо заметить, частично на багах апи и реверс-инженеринге) и на стабильность интерфейсов в требуемых масштабах времени расчитывать не приходится.
                      Т.о. придется таки тратить немало усилий по поддержке моделей.
                    +3
                    Давайте по пунктам чем python лучше php для подобной задачи? Это я на основе опыта перехода с асинхронного python на gearman + php спрашиваю.
                      +2
                      Тем, что код, написанный автором, а так же множество других сопряженных с ним фич, реализованы в стандартном модуле multiprocessing, который широко протестирован, стабилен и поддерживается самим питоном. Простым выбором нужного класса Pool-а вы можете выбрать мультипроцессную, мультизадачную или кооперативную многозадачность. Они написаны и отлажены в тысячи проектов.

                      В PHP — чисто из практических соображений, скажите честно, вы бы хотели иметь в проекте код, подобный коду из статьи, сопровождать и поддерживать его?
                        0
                        Передергиваете.
                        Почему у вас в питоне вдруг модуль, а в пхп код, который нужно поддерживать?
                        Не авторский, так чей другой, но в любом случае работающий как отдельная библиотека.
                          +1
                          Я дал ссылку на модуль из стандартной библиотеки. Он из builtins питона.
                            –1
                            А в чем идейная разница между модулем из стандартной библиотеки и подключаемой либой, которую не ты поддерживаешь?
                            По аналогии c c++ std:: и boost::
                              0
                              Вы абсолютно правы в том, что между std:: и boost: разницы мало. Обе поддерживаются сообществом, обе используются в тысячах проектов. Обе имеют обширную и исчерпывающую документацию.

                              Сейчас речь идет о модуле из стандартной библиотеке и решении из статьи. Вот лично вы хотели бы иметь код из данной статьи у себя в проекте, поддерживать и сопровождать его? Взяли бы вы этот код в продакшн-решение, ошибка в котором стоит реальных денег?

                              Или же с чисто практической точки зрения выгодней взять стандартное решение, которое поддерживается сообществом, которое протестировано десятками юниттестов и апробировано в тысячах проектов?
                                +1
                                Резюмируя: одному решению вы доверяете сильно больше другого.
                                MadJeck спрашивал о другом, не об использовании конкретно кода из данной статьи.

                                Что касается вашего вопроса:
                                В ближайшие дни я внимательно изучу код автора, и, если он оправдает надежды (ну или один раз подправив до кондиции) использую его без смущения. Т.к. в моем случае (есть выше) это будет практичнее, несмотря на то, что питон меня не страшит.
                        • UFO just landed and posted this here
                            0
                            А откуда по вашему появляются многие стандартные используемые всеми библиотеки? Это такой же код написанный такими же программистами, а не принесенный богами на нашу грешную землю. Просто этот код уже используется, проверен людьми, проектами и временем. Новые решения тоже требуют проверки, может сейчас это и костыль, но кому-то он может быть полезен и со временем станет такой же используемой всеми библиотекой ) И дело не в том лучше язык или хуже, важно что в нем есть такое решение, которое может кому-то понадобиться в разных ситуациях.
                        +1
                        Для перекодировки превьюшек достаточно стадартизировать входные и выходные данные, которые потом обработать на любом ЯП. Синхронизацию можно сделать тысячью способов: БД, пайп, сокет, много еще чего. Полные модели для перекодировки… Зачем?
                          0
                          Это был пример. Вероятно, не самый удачный.
                            0
                            А какой более удачный?
                              +1
                              Вытягивание и обработка событий, привязанных к пользователям, из совокупности источников в виде XMLRPC API и внешней бд, структура которых может непредсказуемо меняться. Для них существуют успешно функционирующие и обновляемые модели в php, совсем не в виде AR. Push'а как такового нет, только pull. Событий много.

                              Решение в виде переписывания модели на питоне — 1. сложно само по себе, 2. потребует дополнительной поддержки
                              Решение в виде расслоения с подготовкой данных питоном и последующей обработкой php — 1. сложно, 2. все-таки потребует механизмов фонового выполнения php кода.
                                +1
                                Это реальный пример? Возьмите меня на работу!
                          0
                          обычно исходя из задачи выбирают технологии для её решения, а не как вы за уши притянули кейс, что без нецелевом использоваании php не обойдешься
                            0
                            Кейс вполне реальный. Если и не большинство сайтов на PHP, то много уж точно и задачи асинхронной обработки на них возникают нередко, хотя бы Q&A можно посмотреть. Во многих случаях реализация не на PHP вызовет задержки реализации, увеличения стоимости поддержки и развития. Хорошо, конечно, если у вас разработчики моментально переключаются между десятком языков, знают какой в каких ситуациях использовать (и даже поважнее знания собственно языка знания есть ли в его стандартной библиотеки нужные функции из коробки или сторонние популярные модули/библиотеки/фреймворки), знают все плюсы и минусы каждого из более-менее разумного варианта. А кроме разработчиков ещё и админы с легкостью администрируют гетерогенную среду. Но зачастую всего этого нет и надо или нанимать, или обучать, но тогда решение идеальное с точки зрения технического обоснования может быть далеко не идеальным с точки зрения экономического. Исходить, с точки зрения бизнеса, нужно прежде всего из доступных ресурсов на решение задачи и дохода, который её решение принесёт.
                              +1
                              >>Исходить, с точки зрения бизнеса, нужно прежде всего из доступных ресурсов на решение задачи и дохода, который её решение принесёт.

                              Согласен с вами
                          –1
                          Что значит «не предназначен для этой цели»? Если инструмент позволяет решать задачи — почему «не предназначен»?

                          Если уж на то пошло, то Вы, видимо, не применяете js на сервере, а многие используют и довольны результатом. Ну а применять файлы для хранения данных (включая кеш) — вообще великий грех?

                          И не надо говорить про микроскоп и гвоздь — если я кувалдой забиваю гвозди быстрее, чем самым навороченным молотком, мне подходит именно кувалда, хотя «она не предназначена для этого»…
                            0
                            В третьем абзаце вы ответили на вопросы первого абзаца.

                            А второй абзац из области ясновидения и домыслов. Без комментариев.
                            +2
                            Это почему не предназначен? Все используемые функции (в основном из ext/pcntl и ext/posix) являются врапперами соответствующих сишных функций. Практически 1 к 1. Может, C не предназначен для этой цели?
                              +2
                              Ну как же! Нет специальной ООП обёртки для всего этого добра в стандартной либе. Ужас, приходится иметь дело напрямую с биндингами к функциям ядра.
                              +2
                              То что он для WEB, не мешает на нём отлично писать консольные скрипты и приложения, т.к. с PHP 5.3 допилили менеджер памяти, добавили сборку мусора и вообще довели CLI до состояния, когда оно особо то и не отличается от того же голого python или ruby. Другое дело что наработок и библиотек практически нету, но это не вина самого языка. Да и нету такой большой необходимости этим заниматься вне WEB проектов. А когда у вас WEB проект, да на Zend/Symphony/Yii, то там есть консольные компоненты и утилиты для запуска таких процессов в контексте проекта (насчёт Zend не уверен, Symphony насколько я знаю такое имеет, а с Yii сам работаю и у него есть 2 вида компонент: CConsoleCommand и CConsoleApplication). Они просты и большее от них редко требуется. А мне вот потребовалось и я быстро и легко допилил себе консольный демон с форками, сигналами и прочей лабудой — пиши тока логику и не парься — ничего сложного там нету. Нужно просто знать и понимать как нужно писать приложения такого рода.

                              Код у автора статьи далеко не лучший пример — это скорее показать что можно, но дальше нужно думать самому. Да и без фреймворка будет уныло.

                              З.Ы. А ещё есть phpDaemon
                            +3
                            может не в полной мере как в статье, но для управления и демонизации php воркеров можно использовать supervisord (правда решение не на php)
                          +4
                          много кода, мало описания(
                            0
                            да, мне тоже так показалось, но я постарался дать «говорящие» имена методам и переменным, чтобы можно было без лишних слов разобраться непосредственно в коде.
                              +1
                              Всё же лучше было описать что умеют делать ваши классы и их технологические плюсы.

                              Мне действительно интересна тема системного программирования в php, но читать длинную простыню кода совершенно нет никакого желания.

                              Пожалейте нас… :)

                              PS Спасибо за статью, будем тестировать.
                                +1
                                Поддержу предыдущего оратора. Имхо, на Хабре стоит приводить только наиболее интересные фрагменты кода, если его длина больше сотни-другой строк. А даже если не больше, то всё равно стоит подумать о фрагментах, а не о «цитировании» полных файлов. При всём уважении к желанию (и смелости) поделиться своими наработками на Хабре, это не место для непосредственного обмена кодом, даже хорошо прокомментированным (про комментарии вида ./*while ($this->isRunning()) {
                                echo '.';
                                sleep(1);
                                }*/ промолчу).

                                Достаточно ссылок на кодохостинги, а, необходимы, чуть ли не главное, комментарии более подробные, чем в коде — как пришли именно к такому варианту, какие были альтернативы, чем не устроили существующие решения и т. п. имхо, это принесёт больше пользы, чем почти голый код, даже лично вам — «критики» укажут на ошибку (по их мнению ошибку) в точном месте, где ваши и их рассуждения разошлись.

                                P.S. А почему именуете приватные переменные с префиксом _?
                                  0
                                  учту.

                                  отвечу на последний вопрос: исторически полюбившийся метод именования всех непубличных переменных таким образом. В подсознании возникает четкое понимание — раз подчеркнуто значит это чье-то и лучше напрямую не лезть.
                                    0
                                    >P.S. А почему именуете приватные переменные с префиксом _?

                                    Не только переменные, но и методы.

                                    Такой стиль используется в Zend Framework, хотя и не является частью их стандарта кодирования (по-крайней мере явно).
                                +2
                                Хорошо бы это на основе/в рамках symfony-process сделать.
                                  0
                                  спасибо за ссылку, постараюсь ознакомиться
                                  0
                                  Большое спасибо, очень полезно!
                                    +2
                                    Автору спасибо!
                                    [sarcasm]Развели флейм в каментах, как обычно. Каждому не терпится сообщить, что с PHP он завязал и теперь по взрослому пишет на Python — очень ценная информация. [/sarcasm]
                                      0
                                      +1. И вообще, такое ощущение, что теперь любой пост про пхпх будет заканчиваться каментами, чем питон круче пхп
                                      0
                                      Побуду некропостером :)

                                      > while ($line = fgets($this->_pipes[$index])) {

                                      С этим кодом есть одна проблема — он будет продолжать читать пайп до тех пока не встретиться EOF, для proc_open это возможно только при закрытии пайпа — т.о. о полноценной двусторонней коммуникации между процессами можно забыть. Точно такая же проблема характерна для и для fread и остальных функций — если указано кол-во байт большее чем доступно в данный момент, чтение будут продолжать пока все они не будут получены или не встретиться EOF => получить все доступные сейчас данные сейчас просто невозможно (в читающем процессе нет возможности узнать сколько байт было послано).

                                      Единственное извращение которое удалось придумать — обмениваться отдельными строками с сериализованными данными (с обязательным добавлением EOL-а при записи в пайп). Если у кого-то есть другие идеи буду рад услышать…
                                        0
                                        Такой проблемы нет. Для этого в коде используется stream_set_blocking
                                          0
                                          Ага, ошибся. Но все оказалось проще (или скорее хуже) — для пайпов открытых через proc_open stream_set_blocking вообще не работает под win :( (https://bugs.php.net/bug.php?id=47918, bugs.php.net/bug.php?id=34972, bugs.php.net/bug.php?id=51800) хоть бы написали что-ли где об этом… Впрочем под win так же присутствует «взаимная блокировка» (stream_select вообще странные результаты возвращает) при одновременном(ой) чтении(записи) в STDOUT и STDERR (https://bugs.php.net/bug.php?id=51800) что делает взаимодействие между процессами практически невозможным (осталось поэкспериментировать с файлами вместо пайпов).
                                        0
                                        У меня почему-то 100% нагрузки на одно ядро процессора (если ядер несколько), но только если процесс-менеджер запускается через crontab. Если просто с bash вызвать, то такой проблемы нет. CentOS 6.6 64bit, VPS (один на firstvds.ru, OpenVZ, второй на DigitalOcean, KVM). И на выделенном сервере с CentOS 6.5 + CloudLinux тоже самое.

                                        Only users with full accounts can post comments. Log in, please.