waLongActionController

Контроллер для выполнения длительных действий без прерывания из-за серверных ограничений

Содержание...

Базовый класс — waController.

Этот контроллер позволяет реализовать потенциально длительные операции, когда нет возможности избежать срабатывания серверного ограничения на длительность исполнения PHP-скриптов (max_execution_time). Контроллер работает в связке с браузерным скриптом, который должен время от времени отправлять запросы к серверу для получения информации о статусе работы контроллера.

Схематический пример JavaScript-скрипта, отправляющего запросы к контроллеру:

var url = '...'; //URL, обрабатываемый контроллером waLongActionController
var processId = undefined;

var step = function (delay) {
    //интервал запросов к серверному контроллеру
    delay = delay || 2000;
    
    var timer_id = setTimeout(function () {
        $.post(
            url,
            {processId: processId},
            function (r) {
                if (!r) {
                    step(3000);
                } else if (r && r.ready) {
                    //работа контроллера завершена
                    //устанавливаем индикатор выполнения процесса на значение 100%
                    $('.progressbar .progressbar-inner').css({
                        width: '100%'
                    });
                    $('.progressbar-description').text('100%');
                    
                    //получаем отчет о выполненной работе
                    $.post(url, {processId: processId, cleanup: 1}, function (r) {
                        if (r.report) {
                            setTimeout(function () {
                                //показываем отчет пользователю
                                $('.progressbar').hide();
                                $('.report').show();
                                $('.report').html(r.report);
                            }, 1000);
                        }
                    }, 'json');
                } else if (r && r.error) {
                    //если произошла ошибка, показываем ее текст
                    //и прекращаем работу
                    $('.errormsg').text(r.error);
                } else {
                    //если все нормально, обновляем значение индикатора
                    if (r && r.progress) {
                        var progress = parseFloat(r.progress.replace(/,/, '.'));
                        $('.progressbar .progressbar-inner').animate({
                            'width': progress + '%'
                        });
                        $('.progressbar-description').text(r.progress);
                        $('.progressbar-hint').text(r.hint);
                    }
                    
                    //если контроллер вернул предупреждение,
                    //показываем его пользователю и продолжаем работу
                    if (r && r.warning) {
                        $('.progressbar-description').append('<i class="icon16 exclamation"></i><p>' + r.warning + '</p>');
                    }

                    //переходим к следующему запросу к серверу
                    step();
                }
            },
            'json'
        ).error(function () {
            //если при выполнении POST-запроса возникла ошибка
            //повторим попытку через несколько секунд
            step(3000);
        });
    }, delay);
};

//первый запуск скрипта
$.post(url, {}, function (r) {
    if (r && r.processId) {
        processId = r.processId;
        step();
    } else if (r && r.error) {
        $('.errormsg').text(r.error);
    } else {
        $('.errormsg').text('Server error');
    }
}, 'json').error(function () {
    $('.errormsg').text('Server error');
});

Каждый запрос браузерного скрипта приводит к запуску отдельного процесса на сервере. Из всех процессов, порожденных контроллером, один (self::TYPE_RUNNER) выполняет полезную работу — либо до ее полного завершения, либо до его преждевременного прекращения из-за срабатывания ограничения max_execution_time, а остальные (self::TYPE_MESSENGER) отправляют в браузер информацию о текущем статусе выполнения операции.

Основная логика контроллера должна быть разбита на отдельные фрагменты, каждый из которых гарантированно может быть выполнен до достижения ограничения на время исполнения PHP-скриптов. Логика каждого такого фрагмента должна быть описана в методе step.

Ниже перечислены методы, которые должны быть реализованы разработчиком в собственном контроллере.

Методы

  • finish

    Определяет, можно ли очистить связанные с выполнением операции временные данные (файлы) после ее завершения.

  • info

    Возвращает в браузер информацию о статусе выполнения операции.

  • init

    Инициализация значений, которые могут использоваться в работе контроллера.

  • isDone

    Определяет факт завершения операции.

  • step

    Логика выполнения отдельного фрагмента операции.

protected function finish ($filename)

Определяет, можно ли очистить связанные с выполнением операции временные данные (файлы) после ее завершения.

Параметры

  • $filename

    Имя файла, гарантированно содержащего корректную информацию о последнем успешно выполненном фрагменте операции.

Пример

protected function finish($filename)
{
    //Для надежности в некоторых ситуациях вы можете здесь проанализировать
    //содержимое файла $filename
    
    $this->info();
    if ($this->getRequest()::post('cleanup')) {
        return true;
    }
    return false;
}

protected function info ()

Возвращает в браузер информацию о статусе выполнения операции.

Пример

protected function info()
{
    $interval = 0;
    if (!empty($this->data['timestamp'])) {
        $interval = time() - $this->data['timestamp'];
    }
    $response = [
        'time'      => sprintf('%d:%02d:%02d', floor($interval / 3600), floor($interval / 60) % 60, $interval % 60),
        'processId' => $this->processId,
        'progress'  => 0.0,
        'ready'     => $this->isDone(),
        'offset'    => $this->data['offset'],
        'hint'      => $this->data['hint'],
    ];
    $response['progress'] = empty($this->data['products_total_count']) ? 100 : ($this->data['offset'] / $this->data['products_total_count']) * 100;
    $response['progress'] = sprintf('%0.3f%%', $response['progress']);

    if ($this->getRequest()->post('cleanup')) {
        $response['report'] = $this->report();
    }

    echo json_encode($response);
}

protected function init ()

Инициализация значений, которые могут использоваться в работе контроллера.

Пример

protected function init()
{
    $product_model = new shopProductModel();

    $this->data['products_total_count'] = $product_model->countAll();
    $this->data['offset'] = 0;
    $this->data['product_id'] = null;
    $this->data['product_count'] = 0;
    $this->data['timestamp'] = time();
    $this->data['hint'] = _wp('Starting data update...');
}

protected function isDone ()

Определяет факт завершения операции. Как только метод вернет true, прекращается выполнение основной логики контроллера в методе step.

Пример

protected function isDone()
{
    return $this->data['offset'] >= $this->data['products_total_count'];
}

protected function step ()

Логика выполнения отдельного фрагмента операции.

Пример

protected function step()
{
    static $products;
    if (empty($products)) {
        $products = ...; // получаем новые данные для обновления записей в БД
        $this->data['hint'] = _wp('Applying new values...');
    }

    static $product_model;
    if (empty($product_model)) {
        $product_model = new shopProductModel();
    }

    // ограничиваем количество обновляемых записей база данных небольшим
    // числом, чтобы при их обновлении гарантированно не выйти за размер
    // ограничения max_execution_time
    $chunk_size = 10;
    $chunk = array_slice($products, $this->data['offset'], $chunk_size);
    
    foreach ($chunk as $product) {
        $product_model->updateById($product['id'], [
            'field_name' => $product['field_name'],
        ]);
        if ($this->data['product_id'] != $product['id']) {
            sleep(0.2);
            $this->data['product_id'] = $product['id'];
            $this->data['product_count'] += 1;
        }
        $this->data['offset'] += 1;
    }

    return true;
}