Bettering the Batch API

Batch of Cookies

Whenever I hear the word batch, I think of a nice, delicious batch of homemade cookies, and while the Drupal Batch API might not taste as good, understanding how to use it is an essential part of every Drupal developer's toolkit. 

If you've been a web developer for awhile, you've probably come across a max execution time error. These happen when the page request is taking too long to load. Normally, this is a good thing making sure one user doesn't lock up the whole server and prevent other users from accessing the site. However, when you're trying to do a lot of processing as soon as a user clicks a button, this timeout limit can be a pain to deal with. Sure, you can always up the memory on your virtual machine or extend the max timeout variable, but if you're using shared hosting or just have limited resources, these hacky ways of fixing the issue might not even be options.

In comes the Batch API to the rescue. The Batch API breaks up these memory intensive processes into chunks so that your server and the user don't experience timeout or memory issues. By default, the Batch API also provides a nice progress bar to tell the user where they're at in completing the batch job. 

So, how do you actually use this API in order to not have pesky timeout issues and provide your users a better experience? Well, the batching process happens in about three steps. First, you have to define the batch. It will probably look something like the following code:

    // Chunk batch work into groups.
    $operations = array();
    foreach ($batch_chunks as $chunk) {
      $operations[] = array('your_module_process_batch', array($chunk));
    }

    // Set batch operation and redirect when done.
    $batch = array(
      'title' => t('Starting Batch'),
      'operations' => $operations,
      'finished' => 'your_module_batch_finished',
      'init_message' => t('Initializing...'),
      'progress_message' => t('Operation @current out of @total.'),
      'error_message' => t('Batch failed.'),
    );
    batch_set($batch);
    // If not in a form, process batch. 
    batch_process('admin/settings/bundles/list');

First comes the batch operations. You can think of these as the individual pieces of work that you want done sequentially. While you don't have to set the "operations" batch array key first, I think it looks nicer and makes sense to define them before before the reset of the batch. You can have more than one batch operation callback, like "your_module_bath_process2", and more than one variable passed into that operation, like "array('your_module_process_batch2', array($chunk, $other_variable))", but having one batch function and one passed variable is pretty common. 

It's important to note the use of "batch_process('redirect/path')" here. You don't need to set the batch to be processed if you are setting the batch in a form callback, but if the batch is set in any other way, you'll need to use this function to start the batch. The parameter it takes is the page you want the user to be taken to when the batch finishes. 

The title, init_message, and error_message are pretty self explanatory, but the finished and progress_message keys need a little explaining. The progress message uses variables from each time a batch operation is looped through. You can use placeholders, like "@current, @remaining, @total and @percentage". If you want to pass some extra messaging, you can do so with the $context variable which is passed between each iteration of the batch:

function your_module_process_batch($node, &$context) {
  // Do work here...

  // Pass progress back to message screen.
  $context['message'] = t('Now processing %node', array('%node' => $node->title));
  $context['results'][] = $node;
}

Once the batch is finished, it's handed off to the finished function callback where the user is informed of something or cleanup is done. 

function your_module_batch_finished($success, $results, $operations) {
  if ($success) {
    // Tell user something.
   drupal_set_message('Processed' . count($results) . 'nodes.');
  } else {
    // Output error message.
  }
}

The "$success" variable is a boolean where TRUE means that the batch completed successfully and FALSE means the batch failed. The "$results" variable is taken from "$context['results']" and contains anything you put in there over the course of the batch process. Usually, this variable contains results that you want to inform the user about. 

There are many examples of using the Batch API for Drupal 7 when you Google it, so I won't go into anything else in detail...except for the oversight that made me go insane staring at my computer screen and running the batch process over and over. 

I would get a progress message relating to the number of operations I had queued up but none of those operation callbacks were executing. If I had three batch operations, it would say it was going through three operations, and if I set two operations, it would say it was going through two operations. However, nothing I put in the operation callbacks was getting processed. I tried putting debugging statements every place, and even using Xdebug to follow what Drupal was doing for the batch function's lifecycle. 

Nothing seemed to work. I even tried in vain to use the file key in the batch setting process.

    $batch = array(
      'title' => t('Starting Batch'),
      'operations' => $operations,
      'finished' => 'your_module_batch_finished',
      'init_message' => t('Initializing...'),
      'progress_message' => t('Operation @current out of @total.'),
      'error_message' => t('Batch failed.'),
      'file' => 'my_file.admin.inc',
    );

I happened to be in an ".admin.inc" file since this batch was set in an admin page function. I had my batch processing and finished functions in the same file, which is why I didn't use the file key at first. Even if I moved the batch functions into another file, still nothing happened. I tried removing the ".inc" at the end, because I saw that not included in another example, and still nothing...that is until I put my batch function in the ".module" file. 

I don't exactly know why this was the case for me, but I wanted to share this tidbit in case it saves someone else some time. I'd have to investigate the internal batch code to see why this was the case, but all I can say is that I'm glad my batch was finally processing. 

Like I said, please Google "Batch API Drupal 7" to get many other blog posts about this topic, and you'll be happy to know that the Batch API is included in Drupal 8 as well so all your learning now won't be in vain in the the future.