Last updated March 25, 2015. Created on October 3, 2007.
Edited by mogtofu33, Satraa, drupalshrek, joachim. Log in to edit this page.

Here's an example of how to use the Batch API, originally introduced in Drupal 6. In this example, you would probably call batch_example() from a form submit handler, where the form submission provided the $options you want to use to update the nodes.

PS! This is only an example. Don't forget to actually read the API documentation.

/**
 * The $batch can include the following values. Only 'operations'
 * and 'finished' are required, all others will be set to default values.
 *
 * @param operations
 *   An array of callbacks and arguments for the callbacks.
 *   There can be one callback called one time, one callback
 *   called repeatedly with different arguments, different
 *   callbacks with the same arguments, one callback with no
 *   arguments, etc. (Use an empty array if you want to pass 
 *   no arguments.)
 *
 * @param finished
 *   A callback to be used when the batch finishes.
 *
 * @param title
 *   A title to be displayed to the end user when the batch starts. The default is 'Processing'.
 *
 * @param init_message
 *   An initial message to be displayed to the end user when the batch starts.
 *
 * @param progress_message
 *   A progress message for the end user. Placeholders are available.
 *   Placeholders note the progression by operation, i.e. if there are
 *   2 operations, the message will look like:
 *    'Processed 1 out of 2.'
 *    'Processed 2 out of 2.'
 *   Placeholders include:
 *     @current, @remaining, @total and @percentage
 *
 * @param error_message
 *   The error message that will be displayed to the end user if the batch
 *   fails.
 *
 * @param file
 *   Path to file containing the callbacks declared above. Always needed when
 *   the callbacks are not in a .module file.
 *
 */
function batch_example($options1, $options2, $options3, $options4) {
  $batch = array(
    'operations' => array(
      array('batch_example_process', array($options1, $options2)),
      array('batch_example_process', array($options3, $options4)),
      ),
    'finished' => 'batch_example_finished',
    'title' => t('Processing Example Batch'),
    'init_message' => t('Example Batch is starting.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Example Batch has encountered an error.'),
    'file' => drupal_get_path('module', 'batch_example') . '/batch_example.inc',
  );
  batch_set($batch);

  // If this function was called from a form submit handler, stop here,
  // FAPI will handle calling batch_process().

  // If not called from a submit handler, add the following,
  // noting the url the user should be sent to once the batch
  // is finished.
  // IMPORTANT: 
  // If you set a blank parameter, the batch_process() will cause an infinite loop

  batch_process('node/1');
}

/**
 * Batch Operation Callback
 *
 * Each batch operation callback will iterate over and over until
 * $context['finished'] is set to 1. After each pass, batch.inc will
 * check its timer and see if it is time for a new http request,
 * i.e. when more than 1 minute has elapsed since the last request.
 * Note that $context['finished'] is set to 1 on entry - a single pass 
 * operation is assumed by default.
 *
 * An entire batch that processes very quickly might only need a single
 * http request even if it iterates through the callback several times,
 * while slower processes might initiate a new http request on every
 * iteration of the callback.
 *
 * This means you should set your processing up to do in each iteration
 * only as much as you can do without a php timeout, then let batch.inc
 * decide if it needs to make a fresh http request.
 *
 * @param options1, options2
 *   If any arguments were sent to the operations callback, they
 *   will be the first arguments available to the callback.
 *
 * @param context
 *   $context is an array that will contain information about the
 *   status of the batch. The values in $context will retain their
 *   values as the batch progresses.
 *
 * @param $context['sandbox']
 *   Use the $context['sandbox'] rather than $_SESSION to store the
 *   information needed to track information between successive calls to
 *   the current operation. If you need to pass values to the next operation
 *   use $context['results'].
 *
 *   The values in the sandbox will be stored and updated in the database
 *   between http requests until the batch finishes processing. This will
 *   avoid problems if the user navigates away from the page before the
 *   batch finishes.
 *
 * @param $context['results']
 *   The array of results gathered so far by the batch processing. This
 *   array is highly useful for passing data between operations. After all
 *   operations have finished, these results may be referenced to display
 *   information to the end-user, such as how many total items were
 *   processed.
 *
 * @param $context['message']
 *   A text message displayed in the progress page.
 *
 * @param $context['finished']
 *   A float number between 0 and 1 informing the processing engine
 *   of the completion level for the operation.
 *
 *   1 (or no value explicitly set) means the operation is finished
 *   and the batch processing can continue to the next operation.
 *
 *   Batch API resets this to 1 each time the operation callback is called.
 */
function batch_example_process($options1, $options2, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['current_node'] = 0;
    $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT nid) FROM {node}')->fetchField();
  }

  // For this example, we decide that we can safely process
  // 5 nodes at a time without a timeout.
  $limit = 5;

  // With each pass through the callback, retrieve the next group of nids.
  $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit);
  while ($row = db_fetch_array($result)) {

    // Here we actually perform our processing on the current node.
    $node = node_load($row['nid'], NULL, TRUE);
    $node->value1 = $options1;
    $node->value2 = $options2;
    node_save($node);

    // Store some result for post-processing in the finished callback.
    $context['results'][] = check_plain($node->title);

    // Update our progress information.
    $context['sandbox']['progress']++;
    $context['sandbox']['current_node'] = $node->nid;
    $context['message'] = t('Now processing %node', array('%node' => $node->title));
  }

  // Inform the batch engine that we are not finished,
  // and provide an estimation of the completion level we reached.
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch 'finished' callback
 */
function batch_example_finished($success, $results, $operations) {
  if ($success) {
    // Here we do something meaningful with the results.
    $message = t('@count items successfully processed:', array('@count' => count($results)));
    // $message .= theme('item_list', $results);  // D6 syntax
    $message .= theme('item_list', array('items' => $results));
    drupal_set_message($message);
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = t('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)));
    drupal_set_message($message, 'error');
  }
  
}

Another example that might be of interest is GiantRobot / csvimport which uses file upload and Batch API to parse a CSV file line by line. Also an updated version for Drupal 7 is available.

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

tsphethean’s picture

Is it possible to execute a batch process, but then monitor the progress of the batch and if the duration exceeds a given time, fire the batch complete function and re-execute the rest of the batch in the background?

In effect the user experience would be:

1. User clicks button which executes processing of batch, and is taken to the /batch page
2. The tasks process, but if the total execution time reaches 30 seconds before the batch is complete the user is taken to a summary page to be shown what has been processed so far
3. Whilst reviewing what has been completed so far, the remaining tasks (if any) from the batch execute in the background.
3a. If the whole batch completed in (2) then the user is still taken to the same summary page, and the list shows all the tasks which were completed.

I believe the background part can be done by setting the batch to progressive = FALSE, but I've read there are some problems doing this in D6 - is the above possible?

Any help much appreciated.

gielfeldt’s picture

Background Process contains the module Background Batch, which runs a batch job in the background. Maybe not exactly as you describe, but check it out.

ppetrid’s picture

'Sandbox' results in __PHP_Incomplete_Class Object error when storing custom class instances and it failes silently.. I figured it out the hard way! I posted on my blog on how to resolve this at http://blog.yawd.eu/2011/use-classes-static-variables-drupal-6-batch-api/. I also included a couple of notes based on my adventure with the Batch API. Hope this helps!

Sinan Erdem’s picture

Hello,

I have a module to make an operation by batch api. When I activate it on the browser, I can see the progress bar moving and it does its job ok. But if I put the function in a cronjob, I know that it starts, but doesn't do anything. Does batch only work when it is visible on a browser? Here is my module code:


function update_cnr_cron(){

  $batch = array(
    'operations' => array(
      array('update_cnr_updater', array())
      ),
    'finished' => 'batch_example_finished',
    'title' => t('Updating cnr nodes'),
    'init_message' => t('Update cnr is starting.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Update cnr has encountered an error.'),
  );
  batch_set($batch);

  batch_process('node/1');
Sinan Erdem’s picture

@gielfeldt, your offered module for the other question, I think, also works for my case. On the module definition page:

The bundled module Background Batch runs all batch jobs as a background process.

I am now checking the progress of the batch on page admin/settings/batch/overview , and it looks it is working...

Thanks..

pierrot’s picture

I'm trying to make this work in a custom module.
The problem is that my batch is kind of heavy and need to call other functions, also written in the same .module .
The batch works properly, however $context seems to be lost : progress bar is not updating and keep showing the init_message text.
As all my functions are located in the same .module file, i presume i don't need to include any file in the batch array. But then how to include my other functions in the batch process without losing $context?

Tyler Pepper’s picture

Are you setting $context['finished'] as the batch progresses? I know I missed that a few times and my progress bar didn't update.

Employee at Fenix Solutions.

steamx’s picture

Hi,
i know this answer comes rather late, but did you try this with all user permissions?

Currently I am working on a project where a copy of our drupal only seems to allows for batch operation access to the administrator even though only our hardware and a few settings (not drupal related) changed.

A.Zahr’s picture

Did you find a fix? Please reply me, because I'm stuck in the same problem now.

jufran20’s picture

Is it possible to manage all the process as a transaction? (commit, rollback)
In each operation, I save a node, but I need you to save all nodes or if there is an error not save any.
My tables are InnoDB but I don't know how it do.
Thanks

franck0015’s picture

Some times, using drupal_set_message in #finished function isn't the best way to display properly what happened during the batch process. You can redirect to some other page using this code below :

  $batch = array(
    'operations' => array(
      array('batch_example_process', array($options1, $options2)),
      array('batch_example_process', array($options3, $options4)),
      ),
    'finished' => 'batch_example_finished',
    'title' => t('Processing Example Batch'),
    'init_message' => t('Example Batch is starting.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Example Batch has encountered an error.'),
    'file' => drupal_get_path('module', 'batch_example') . '/batch_example.inc',
  );

//Small tweak here, so we get redirected at the end of the process to display a summary
$form_state['redirect'] = '_random_module_display_batch_summary';
$form_state['storage'] = array();

batch_set($batch);

This way, in your function #finished, you can gather data from $results and pass it over $_SESSION. Then, you'll get redirected to another page, where you can theme properly the summary.

Hope I was clear enough.

jphelan’s picture

Just any FYI that took me a little while to figure out. If you are not passing values to your operations your need to still pass an empty array.

  $batch = array(
    'operations' => array(
      array('batch_example_process', array()),
      ),
    'finished' => 'batch_example_finished',
    'title' => t('Processing Example Batch'),
    'init_message' => t('Example Batch is starting.'),
    'progress_message' => t('Processed @current out of @total.'),
    'error_message' => t('Example Batch has encountered an error.'),
    'file' => drupal_get_path('module', 'batch_example') . '/batch_example.inc',
  );
mikedotexe’s picture

Wanted to share my experience to help others.
I have a system where an Excel file is attached to a node, and when saved Batch API goes through each row creating custom nodes with node_save.

However, after it ran the Batch, it forgets to save the original node. (As in, the one containing the Excel file and other important fields.)

By omitting:

batch_process('node/1'); // or whatever path

it worked.
I didn't read the comments closely enough, but the omission of that line is critical for my system.

FYI: I wasn't using FAPI necessarily, I was intercepting the file from hook_node_presave(), so that may also be added to the comments above the batch_process() line.

Hakuna Matata

goatvirus’s picture

Hi there

We are trying to delete about 5000 users from a very large Drupal 6 database, and I have a batch process written that just does an SQL select of the users and then calls user_delete within a loop. not only does it end up crashing from script execution limits (which i expected) it is bombarding the MySQL database server to the point that the server (a VPS) crashes. the webhosting company is enforcing pretty strict memory limits of 3GB max and wants to charge us for adding another 1GB to this just so that this doesn't happen. considering this is essentially a one-time-only operation, this seems like a brute-force way of solving it. and expensive too.

Logic tells me that if the user_delete calls happen with a time delay in between, the site may slow down a bit but at least it won't crash.

Does Batch API work in such a way that you can assign a small delay time between each batch item?

lennartvv’s picture

Yes it's possible by adding a simple PHP function at the end of your batch operation function:

1 second delay:

sleep(1);

1/10 of a second delay:

usleep(100000);

---
Vaerenbergh.com - A Drupal developer's web page

othermachines’s picture