Drupal → Разбираемся с пакетными операциями (Batch API)

19.06.2011

С помощью Batch API можно разбивать длительные операции на части и выполнять каждую часть в отдельном http запросе. Такой механизм позволяет избежать ограничение в 30 секунд (по умолчанию) на выполнение php скриптов.

С работой Batch API сталкивались все, например эта штука используется при установке друпала или обновлении переводов:

Установка Drupal 7

Простейший пример работы с 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 API в Drupal 10

Написанное актуально для
Drupal 7
Похожие записи

Комментарии

Николай
21.12.2011, 12:39

Здравствуйте! Подскажите пожалуйста, как узнать какие функции из очереди (batch queue) выполнились с ошибкой, если такая возможность есть?. И есть ли к ним доступ в функции _batch_finished() Спасибо.

Подскажите пожалуйста, как узнать какие функции из очереди (batch queue) выполнились с ошибкой

массив $context['results'] в вашем полном распоряжении на каждом шаге для каждой операции

Frantsuzzz
21.08.2012, 00:06

Спасибо за информацию. Пойду пробовать)

Павел
03.03.2013, 14:14

Здравствуйте!
Спасибо за информацию.
Только интересует, как после завершения Batch вывести результат в новой форме, а не на той с которой был запущен Batch?
Спасибо.

Гость
11.06.2013, 21:14

как после завершения Batch вывести результат в новой форме, а не на той с которой был запущен Batch?

Для этого используется batch_process('redirect/here');

Игорь
28.03.2014, 08:12

Прикольная штука! Вчера запустил batch он пару часиков отработал, ну и закрыл браузер, выключил комп. А сегодня включил, открыл браузер (у меня открываются вкладки которые были до закрытия) и batch продолжил как ни в чем не бывало дальше работать! Только время затраченное, показывает от вчерашнего старта: 2ч + 8ч (простоя) = 10ч. А я хотел опять запускать с начала...)))

У меня после выполнения batch1 операций должен запуститься новый batch2. Аналогично, после завершения batch2 запуск batch3. При этом первый batch1 запускается по крону. Всё хорошо работает если запустить крон вручную, но если крон запускается автоматически на хостинге, то выполняется только первый batch1, а остальные молчат. Вы не сталкивались с подобной задачей поочерёдного запуска пакетных операций? В чем может быть загвоздка?

kill_windows
04.12.2014, 16:33

В $batch еще может понадобится указать путь к файлу, где располагаются функции mymodule_change_date и mymodule_batch_finished.
$batch['file'] = drupal_get_path('module', 'mymodule') . 'file.inc' - к примеру.
Это особенно касается случаев, если запуск не из формы.

Игорь
26.03.2015, 00:16

Убил 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 операций в список.. в момент работы батч, понял что требуется сделать еще нное количество. Каким образом?

И как это реализовано у вас в Парсере?

Игорь
27.04.2015, 12:28
  // Если операция не завершена, то показываем на каком она этапе
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }

Если $context['finished'] - меньше 1 - едет работа. Равен 1 - завершит выполнение!

В парсере еще процент меняется... сколько бы я не делал прогресс и макс в сэндбоксе, у меня происходят операции в функции... но общий процент не понимаю как менять...

Андрей
05.04.2016, 23:57

Можно ли в одном batch выполнить ещё один? То есть своего рода матрёшка

Василий
09.05.2017, 07:10

А как Batch остановить во время выполнения, например, когда процесс 20/100, а на 21-м происходит какое-то событие, которое делает дальнейшее выполнение процесса бессмысленным?
$context['finished'] = 1; - что-то не спешит его завершать. (Drupal 7)

Евгений
25.06.2017, 21:36

Все таки непонятно как повесить текущий batch на cron.
Нашел на этой странице пример http://xandeadx.ru/blog/drupal/574
но информации совсем мало.
пробую вот так:

function MODULENAME_cron() {
  batch_set($batch);
  $batch = &batch_get();
  $batch['progressive'] = FALSE;
  batch_process('');
}

  $result = db_select('node', 'n')->fields('n', array('nid'))->execute();
  foreach ($result as $row) {
    $operations[] = array('mymodule_change_date', array($row->nid));
  }
 
  $batch = array(
    'operations' => $operations,
    'finished' => 'mymodule_batch_finished',
    'title' => 'Обновление дат',
    'init_message' => 'Подготовка данных',
    'progress_message' => 'Выполнено @current из @total.',
    'error_message' => 'Произошла ошибка.',
  );

но при запуске cron выдает ошибку, подскажите, что не так?
и второй вопрос:
по умолчанию здесь выводится текущее число из всего массива

'progress_message' => 'Выполнено @current из @total.',

но как вставить сюда сообщение: 'выволочено у страницы '.заголовок страницы

но при запуске cron выдает ошибку, подскажите, что не так?

Что не так написано в тексте ошибки

Евгений
25.06.2017, 21:58

Так вот у вас заполнено как надо

$context['message'] = 'Обновлена дата у материала <em>' . check_plain($node->title) . '</em>';

но непонятно как выводить это сообщение во работы batch а не после.

Евгений
25.06.2017, 22:40

Возможно такое вообще нельзя сделать))
вот, когда идет выполнение операции и пишется прогресс выполнения (заполняется полоса прогресса) "обработано столько то из стольки" а можно ли заменить этот текст на 'обновлена дата у "заголовок страницы"'?
то есть пока выполняется операция (а не после завершения) я всегда вижу у каких именно страниц уже обновилась дата

ну как я и написал выше - заполняйте $context['message']

Гость
14.09.2017, 05:56

Можно ли определить выполняется мой батч в данный момент или нет?

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

Скачала использовал друпал-переменную, в которую ставил флаг при запуске и сбрасывал при завершении. Но он не помогает в ситуациях, когда "до завершения не доходят" - закрытие браузера, плохой интернет.

Добавить комментарий