С помощью Batch API можно разбивать длительные операции на части и выполнять каждую часть в отдельном http запросе. Такой механизм позволяет избежать ограничение в 30 секунд (по умолчанию) на выполнение php скриптов.
С работой Batch API сталкивались все, например эта штука используется при установке друпала или обновлении переводов:
Простейший пример работы с Batch API, в котором мы изменим дату создания всех нод на текущую:
/**
* Form builder.
*/
function mymodule_batch_form($form, &$form_state) {
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Начать',
);
return $form;
}
/**
* Form submit callback.
*/
function mymodule_batch_form_submit($form, &$form_state) {
// Подготавливаем данные для операций
$result = db_select('node', 'n')->fields('n', array('nid'))->execute();
// Создаём массив с операциями
foreach ($result as $row) {
$operations[] = array('mymodule_change_date', array($row->nid));
}
batch_set(array(
// Массив операций и их параметров
'operations' => $operations,
// Функция, которая будет выполнена после окончания всех операций
'finished' => 'mymodule_batch_finished',
// Заголовок страницы с прогресс баром.
// Опционально, по умолчанию t('Processing')
'title' => 'Обновление дат',
// Сообщение, показываемое при инициализации.
// Опционально, по умолчанию t('Initializing.')
'init_message' => 'Подготовка данных',
// Сообщение, показываемое при выполнении операций.
// Опционально, по умолчанию t('Completed @current of @total.')
'progress_message' => 'Выполнено @current из @total.',
// Сообщение показываемое при ошибке выполнения операции.
// Опционально, по умолчанию t('An error has occurred.')
'error_message' => 'Произошла ошибка.',
));
// Если Batch API используется не из _submit функции,
// то дополнительно нужно вызвать batch_process();
}
/**
* Batch process callback.
*/
function mymodule_change_date($nid, &$context) {
// Производим манипуляции над нодами
$node = node_load($nid);
$node->created = REQUEST_TIME;
node_save($node);
// Эта информация будет доступна в mymodule_batch_finished
$context['results']['updated_nodes']++;
// Сообщение выводимое под прогресс-баром после окончания текущей операции
$context['message'] = 'Обновлена дата у материала <em>' . check_plain($node->title) . '</em>';
}
/**
* Batch finish callback.
*/
function mymodule_batch_finished($success, $results, $operations) {
if ($success) {
drupal_set_message('Обновлена дата у ' . $results['updated_nodes'] . ' материалов');
}
else {
drupal_set_message('Завершено с ошибками.', 'error');
}
}
Операции в свою очередь можно так же разбивать на части. Например можно сначала обновить заголовки нод, а потом выслать уведомления их авторам (хотя никто не мешает сделать это в одной операции, но для примера пусть будет так):
/**
* Form submit callback.
*/
function mymodule_batch_form_submit($form, &$form_state) {
batch_set(array(
'operations' => array(
array('mymodule_change_date', array()),
array('mymodule_send_notify', array()),
),
'finished' => 'mymodule_batch_finished',
));
}
/**
* Batch process callback.
*/
function mymodule_change_date(&$context) {
// Переменная $context['sandbox'] используется для хранения данных между итерациями
if (empty($context['sandbox'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['prev_nid'] = 0;
$context['sandbox']['max'] = db_select('node')->countQuery()->execute()->fetchField();
}
$nid = db_select('node', 'n')
->fields('n', array('nid'))
->condition('n.nid', $context['sandbox']['prev_nid'], '>')
->orderBy('n.nid')
->range(0, 1)
->execute()
->fetchField();
$node = node_load($nid);
$node->created = time();
node_save($node);
$context['sandbox']['progress']++;
$context['sandbox']['prev_nid'] = $nid;
$context['results']['updated_nodes']++;
$context['message'] = 'Обновлена дата у материала <em>' . check_plain($node->title) . '</em>';
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
/**
* Batch process callback.
*/
function mymodule_send_notify(&$context) {
// Код по аналогии
}
Добавлено 26.03.2016
На самом деле задачи выполняются не всегда в отдельном http запросе, если задача выполнилась быстрее чем за 1 секунду, то сразу же будет выполнена следующая задача, и т.д., пока общее время выполнения не превысит 1 секунду.
Комментарии
Здравствуйте! Подскажите пожалуйста, как узнать какие функции из очереди (batch queue) выполнились с ошибкой, если такая возможность есть?. И есть ли к ним доступ в функции _batch_finished() Спасибо.
массив
$context['results']
в вашем полном распоряжении на каждом шаге для каждой операцииСпасибо, догадался)))
Спасибо за информацию. Пойду пробовать)
Дополню ссылками по теме:
http://api.drupal.org/api/drupal/includes!form.inc/function/batch_set/7
batch_set/7 - по-русски Этот переводчик, в отличии от гугла не рушит отображения кода php
http://api.drupal.org/api/drupal/includes!form.inc/function/batch_proce…
batch_process/7 - по-русски
Здравствуйте!
Спасибо за информацию.
Только интересует, как после завершения Batch вывести результат в новой форме, а не на той с которой был запущен Batch?
Спасибо.
Для этого используется batch_process('redirect/here');
отличный пост
Прикольная штука! Вчера запустил batch он пару часиков отработал, ну и закрыл браузер, выключил комп. А сегодня включил, открыл браузер (у меня открываются вкладки которые были до закрытия) и batch продолжил как ни в чем не бывало дальше работать! Только время затраченное, показывает от вчерашнего старта: 2ч + 8ч (простоя) = 10ч. А я хотел опять запускать с начала...)))
У меня после выполнения batch1 операций должен запуститься новый batch2. Аналогично, после завершения batch2 запуск batch3. При этом первый batch1 запускается по крону. Всё хорошо работает если запустить крон вручную, но если крон запускается автоматически на хостинге, то выполняется только первый batch1, а остальные молчат. Вы не сталкивались с подобной задачей поочерёдного запуска пакетных операций? В чем может быть загвоздка?
Нужно читать логи и сообщения, например watchdog
Будьте внимательней с watchdog()
В $batch еще может понадобится указать путь к файлу, где располагаются функции mymodule_change_date и mymodule_batch_finished.
$batch['file'] = drupal_get_path('module', 'mymodule') . 'file.inc'
- к примеру.Это особенно касается случаев, если запуск не из формы.
Убил 2 дня, чтобы написать парсер прайс-листа (csv или xml не имеет значения). Как всегда зачем то изобретал велосипед. Через 2 дня наконец то дошло - можно же по гуглить(только я яндексю) и тут же готовый модуль, точнее заготовка - https://github.com/GiantRobot/csvimport .
Но это не совсем то. Всё загоняется как и в вашем примере в массив - $operations[]. А если строк миллион? У кого то бывают вообще безумные файлы по 10 гигов... Стало быть надо построчно или побайтово. Вот тут то и была засада - fopen() - открывает - а передать этот дескриптор между итерациями batch оказывается нельзя. Каждый раз надо открывать заново, а быстро найти на чем закончили поможет - ftell() и начать с него fseek(). Ну если ещё немного о производительности, то fgetcsv() тоже не стоит использовать, лучше fgets() и универсальнее, и для xml подойдет. А уж строку в csv - str_getcsv() или xml - xml_parse() - замороченная штука, но работает.
если строк миллион то пользуются вторым листингом, когда в
operations
только одна операция.вместо
$context['sandbox']['prev_nid']
сохранять число обработанных строкВопрос: при создании я забил 5 операций в список.. в момент работы батч, понял что требуется сделать еще нное количество. Каким образом?
И как это реализовано у вас в Парсере?
Если $context['finished'] - меньше 1 - едет работа. Равен 1 - завершит выполнение!
В парсере еще процент меняется... сколько бы я не делал прогресс и макс в сэндбоксе, у меня происходят операции в функции... но общий процент не понимаю как менять...
Спасибо, получилось
Можно ли в одном batch выполнить ещё один? То есть своего рода матрёшка
А как Batch остановить во время выполнения, например, когда процесс 20/100, а на 21-м происходит какое-то событие, которое делает дальнейшее выполнение процесса бессмысленным?
$context['finished'] = 1; - что-то не спешит его завершать. (Drupal 7)
Все таки непонятно как повесить текущий batch на cron.
Нашел на этой странице пример http://xandeadx.ru/blog/drupal/574
но информации совсем мало.
пробую вот так:
но при запуске cron выдает ошибку, подскажите, что не так?
и второй вопрос:
по умолчанию здесь выводится текущее число из всего массива
но как вставить сюда сообщение: 'выволочено у страницы '.заголовок страницы
Что не так написано в тексте ошибки
А со вторым подскажете?
Заполняйте $context['message'] вручную
Так вот у вас заполнено как надо
но непонятно как выводить это сообщение во работы batch а не после.
во время работы это как?
Возможно такое вообще нельзя сделать))
вот, когда идет выполнение операции и пишется прогресс выполнения (заполняется полоса прогресса) "обработано столько то из стольки" а можно ли заменить этот текст на 'обновлена дата у "заголовок страницы"'?
то есть пока выполняется операция (а не после завершения) я всегда вижу у каких именно страниц уже обновилась дата
ну как я и написал выше - заполняйте $context['message']
Можно ли определить выполняется мой батч в данный момент или нет?
У меня довольно долгая операция, к которой имеют доступ несколько пользователей. И если один пользователь операцию запустил и она выполняется, чтоб другим не предлагать её запускать.
Скачала использовал друпал-переменную, в которую ставил флаг при запуске и сбрасывал при завершении. Но он не помогает в ситуациях, когда "до завершения не доходят" - закрытие браузера, плохой интернет.
Добавить комментарий