You are here

background_batch.module in Background Process 6

This module adds background processing to Drupals batch API

@todo Add option to stop a running batch job.

File

background_batch/background_batch.module
View source
<?php

/**
 * @file
 * This module adds background processing to Drupals batch API
 *
 * @todo Add option to stop a running batch job.
 */

/**
 * Default value for delay (in microseconds).
 */
define('BACKGROUND_BATCH_DELAY', 1000000);

/**
 * Default value for process lifespan (in miliseconds).
 */
define('BACKGROUND_BATCH_PROCESS_LIFESPAN', 10000);

/**
 * Default value wether ETA information should be shown.
 */
define('BACKGROUND_BATCH_PROCESS_ETA', TRUE);

/**
 * Implements hook_menu().
 */
function background_batch_menu() {
  $items = array();
  $items['admin/settings/batch/settings'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Settings',
    'weight' => 1,
  );
  $items['admin/settings/batch'] = array(
    'title' => 'Batch',
    'description' => 'Administer batch jobs',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'background_batch_settings_form',
    ),
    'access arguments' => array(
      'administer site',
    ),
    'file' => 'background_batch.pages.inc',
  );
  $items['admin/settings/batch/overview'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Overview',
    'description' => 'Batch job overview',
    'page callback' => 'background_batch_overview_page',
    'access arguments' => array(
      'administer site',
    ),
    'file' => 'background_batch.pages.inc',
    'weight' => 3,
  );
  return $items;
}

/**
 * Implements hook_menu_alter().
 */
function background_batch_menu_alter(&$items) {
  $items['batch'] = array(
    'page callback' => 'background_batch_page',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
    'file' => 'background_batch.pages.inc',
    'module' => 'background_batch',
  );
}

/**
 * Fake batch alter hook for Drupal 6
 */
function background_batch_batch_alter(&$batch) {
  foreach ($batch['sets'] as &$set) {
    if (!empty($set['operations'])) {
      foreach ($set['operations'] as &$operation) {
        $operation = array(
          '_background_batch_operation',
          array(
            $operation,
          ),
        );
      }
    }
  }
  $batch['timestamp'] = microtime(TRUE);
  $batch['batch_altered'] = TRUE;

  // In order to make this batch session independend we save the owner UID.
  global $user;
  $batch['uid'] = $user->uid;
}

/**
 * Run a batch operation with "listening" context.
 * @param $operation
 *   Batch operation definition.
 * @param &$context
 *   Context for the batch operation.
 */
function _background_batch_operation($operation, &$context) {

  // Steal context and trap finished variable
  $fine_progress = !empty($context['sandbox']['background_batch_fine_progress']);
  if ($fine_progress) {
    $batch_context = new BackgroundBatchContext($context);
  }
  else {
    $batch_context = $context;
  }

  // Call the original operation
  $operation[1][] =& $batch_context;
  call_user_func_array($operation[0], $operation[1]);
  if ($fine_progress) {

    // Transfer back context result to batch api
    $batch_context = (array) $batch_context;
    foreach (array_keys($batch_context) as $key) {
      $context[$key] = $batch_context[$key];
    }
  }
  else {
    $batch_context = new BackgroundBatchContext($context);
    $batch_context['finished'] = $context['finished'];
  }
}

/**
 * Process a batch step
 * @param type $id
 * @return type
 */
function _background_batch_process($id = NULL) {
  if (!$id) {
    return;
  }

  // Retrieve the current state of batch from db.
  $data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $id));
  if (!$data) {
    return;
  }
  require_once 'includes/batch.inc';
  $batch =& batch_get();
  $batch = unserialize($data);

  // Check if the current user owns (has access to) this batch.
  global $user;
  if ($batch['uid'] != $user->uid) {
    return drupal_access_denied();
  }

  // Register database update for the end of processing.
  register_shutdown_function('_batch_shutdown');
  timer_start('background_batch_processing');
  $percentage = 0;
  $mem_max_used = 0;
  $mem_last_used = memory_get_usage();
  $mem_limit = ini_get('memory_limit');
  preg_match('/(\\d+)(\\w)/', $mem_limit, $matches);
  switch ($matches[2]) {
    case 'M':
    default:
      $mem_limit = $matches[1] * 1024 * 1024;
      break;
  }
  while ($percentage < 100) {
    list($percentage, $message) = _batch_process();
    $mem_used = memory_get_usage();

    // If we memory usage of last run will exceed the memory limit in next run
    // then bail out
    if ($mem_limit < $mem_used + $mem_last_used) {
      break;
    }
    $mem_last_used = $mem_used - $mem_last_used;

    // If we maximum memory usage of previous runs will exceed the memory limit in next run
    // then bail out
    $mem_max_used = $mem_max_used < $mem_last_used ? $mem_last_used : $mem_max_used;
    if ($mem_limit < $mem_used + $mem_max_used) {
      break;
    }

    // Restart background process after X miliseconds
    if (timer_read('background_batch_processing') > variable_get('background_batch_process_lifespan', BACKGROUND_BATCH_PROCESS_LIFESPAN)) {
      break;
    }
  }
  if ($percentage < 100) {
    background_process_keepalive($id);
  }
}

/**
 * Processes the batch.
 *
 * Unless the batch has been marked with 'progressive' = FALSE, the function
 * issues a drupal_goto and thus ends page execution.
 *
 * This function is not needed in form submit handlers; Form API takes care
 * of batches that were set during form submission.
 *
 * @param $redirect
 *   (optional) Path to redirect to when the batch has finished processing.
 * @param $url
 *   (optional - should only be used for separate scripts like update.php)
 *   URL of the batch processing page.
 */
function background_batch_process_batch($redirect = 'admin/settings/batch/overview') {
  $batch =& batch_get();
  if (isset($batch)) {

    // Add process information
    $url = isset($url) ? $url : 'batch';
    $process_info = array(
      'current_set' => 0,
      'progressive' => TRUE,
      'url' => isset($url) ? $url : 'batch',
      'source_page' => $_GET['q'],
      'redirect' => $redirect,
    );
    $batch += $process_info;

    // Clear the way for the drupal_goto redirection to the batch processing
    // page, by saving and unsetting the 'destination' if any, on both places
    // drupal_goto looks for it.
    if (isset($_REQUEST['destination'])) {
      $batch['destination'] = $_REQUEST['destination'];
      unset($_REQUEST['destination']);
    }
    elseif (isset($_REQUEST['edit']['destination'])) {
      $batch['destination'] = $_REQUEST['edit']['destination'];
      unset($_REQUEST['edit']['destination']);
    }

    // Initiate db storage in order to get a batch id. We have to provide
    // at least an empty string for the (not null) 'token' column.
    db_query("INSERT INTO {batch} (token, timestamp) VALUES ('', %d)", time());
    $batch['id'] = db_last_insert_id('batch', 'bid');

    // Now that we have a batch id, we can generate the redirection link in
    // the generic error message.
    $t = get_t();
    $batch['error_message'] = $t('Please continue to <a href="@error_url">the error page</a>', array(
      '@error_url' => url($url, array(
        'query' => array(
          'id' => $batch['id'],
          'op' => 'finished',
        ),
      )),
    ));

    // Actually store the batch data and the token generated form the batch id.
    db_query("UPDATE {batch} SET token = '%s', batch = '%s' WHERE bid = %d", drupal_get_token($batch['id']), serialize($batch), $batch['id']);
  }
  background_process_start('_background_batch_process_callback', $batch);
}
function _background_batch_process_callback($batch) {
  $rbatch =& batch_get();
  $rbatch = $batch;
  require_once 'background_batch.pages.inc';
  _background_batch_page_start();
}

/**
 * Class batch context.
 * Automatically updates progress when 'finished' index is changed.
 */
class BackgroundBatchContext extends ArrayObject {
  private $batch = NULL;
  private $interval = NULL;
  private $progress = NULL;
  public function __construct() {
    $this->interval = variable_get('background_batch_delay', BACKGROUND_BATCH_DELAY) / 1000000;
    $args = func_get_args();
    return call_user_func_array(array(
      'parent',
      '__construct',
    ), $args);
  }

  /**
   * Set progress update interval in seconds (float).
   */
  public function setInterval($interval) {
    $this->interval = $interval;
  }

  /**
   * Override offsetSet().
   * Update progress if needed.
   */
  public function offsetSet($name, $value) {
    if ($name == 'finished') {
      if (!isset($this->batch)) {
        $this->batch =& batch_get();
        $this->progress = progress_get_progress('_background_batch:' . $this->batch['id']);
      }
      if ($this->batch) {
        $total = $this->batch['sets'][$this->batch['current_set']]['total'];
        $count = count($this->batch['sets'][$this->batch['current_set']]['operations']);
        $elapsed = @$this->batch['sets'][$this->batch['current_set']]['elapsed'];
        $progress_message = $this->batch['sets'][$this->batch['current_set']]['progress_message'];
        $current = $total - $count;
        $step = 1 / $total;
        $base = $current * $step;
        $progress = $base + $value * $step;
        progress_estimate_completion($this->progress);
        $elapsed = floor($this->progress->current - $this->progress->start);
        $values = array(
          '@remaining' => $count,
          '@total' => $total,
          '@current' => $current,
          '@percentage' => $progress * 100,
          '@elapsed' => format_interval($elapsed),
          // If possible, estimate remaining processing time.
          '@estimate' => format_interval(floor($this->progress->estimate) - floor($this->progress->current)),
        );
        $message = strtr($progress_message, $values);
        $message .= $message && $this['message'] ? '<br/>' : '';
        $message .= $this['message'];
        progress_set_intervalled_progress('_background_batch:' . $this->batch['id'], $message ? $message : $this->progress->message, $progress, $this->interval);
      }
    }
    return parent::offsetSet($name, $value);
  }

}

Functions

Namesort descending Description
background_batch_batch_alter Fake batch alter hook for Drupal 6
background_batch_menu Implements hook_menu().
background_batch_menu_alter Implements hook_menu_alter().
background_batch_process_batch Processes the batch.
_background_batch_operation Run a batch operation with "listening" context.
_background_batch_process Process a batch step
_background_batch_process_callback

Constants

Namesort descending Description
BACKGROUND_BATCH_DELAY Default value for delay (in microseconds).
BACKGROUND_BATCH_PROCESS_ETA Default value wether ETA information should be shown.
BACKGROUND_BATCH_PROCESS_LIFESPAN Default value for process lifespan (in miliseconds).

Classes

Namesort descending Description
BackgroundBatchContext Class batch context. Automatically updates progress when 'finished' index is changed.