xandeadx.ru Блог музицирующего веб-девелопера

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

Опубликовано в

С помощью 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 = 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_set($batch);
 
  // Если Batch API используется не из _submit функции,
  // то дополнительно нужно вызвать batch_process();
}
 
/**
 * Batch process callback.
 */
function mymodule_change_date($nid, &$context) {
  // Производим манипуляции над нодами
  $node = node_load($nid);
  $node->created = time();
  node_save($node);
 
  // Эта информация будет доступна в mymodule_batch_finished
  $context['results']['titles'][] = $node->title;
  // Сообщение выводимое под прогресс-баром после окончания текущей операции
  $context['message'] = 'Обновлена дата у материала <em>' . check_plain($node->title) . '</em>';
}
 
/**
 * Batch finish callback.
 */
function mymodule_batch_finished($success, $results, $operations) {
  if ($success) {
    drupal_set_message('Обновлена дата у ' . count($results['titles']) . ' материалов:' . theme('item_list', array('items' => $results['titles'])));
  }
  else {
    drupal_set_message('Завершено с ошибками.', 'error');
  }
}

Операции в свою очередь можно так же разбивать на части. Например можно сначала обновить заголовки нод, а потом выслать уведомления их авторам (хотя никто не мешает сделать это в одной операции, но для примера пусть будет так):

/**
 * Form submit callback.
 */
function mymodule_batch_form_submit($form, &$form_state) {
  $batch = array(
    'operations' => array(
      array('mymodule_change_date', array()),
      array('mymodule_send_notify', array()),
    ),
    'finished' => 'mymodule_batch_finished',
  );
  batch_set($batch);
}
 
/**
 * 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('nid', $context['sandbox']['prev_nid'], '>')
    ->execute()
    ->fetchField();
  $node = node_load($nid);
  $node->created = time();
  node_save($node);
 
  $context['sandbox']['progress']++;
  $context['sandbox']['prev_nid'] = $nid;
  $context['results']['titles'][] = $node->title;
  $context['message'] = 'Обновлена дата у материала <em>' . check_plain($node->title) . '</em>';
 
  // Если операция не завершена, то показываем на каком она этапе
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}
 
/**
 * Batch process callback.
 */
function mymodule_send_notify(&$context) {
  // Код по аналогии
}

Добавлено 26.03.2016

На самом деле задачи выполняются не всегда в отдельном http запросе, если задача выполнилась быстрее чем за 1 секунду, то сразу же будет выполнена следующая задача, и т.д., пока общее время выполнения не превысит 1 секунду.

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

Комментарии RSS

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

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

массив $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_process/7
batch_process/7 - по-русски

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

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

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

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

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

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

Спасибо, получилось

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

Оставить комментарий

Содержимое этого поля является приватным и не будет отображаться публично. Если у вас есть аккаунт в Gravatar, привязанный к этому e-mail адресу, то он будет использован для отображения аватара.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Доступные HTML теги: <a> <i> <b> <strong> <code> <ul> <ol> <li> <blockquote> <em> <s>
  • Строки и параграфы переносятся автоматически.
  • Подсветка кода осуществляется с помощью тегов: <code>, <css>, <html>, <ini>, <javascript>, <sql>, <php>. Поддерживаемые стили выделения кода: <foo>, [foo].

Подробнее о форматировании