You are here

background_batch.module in Background Process 7.2

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 enabled
 */
define('BACKGROUND_BATCH_ENABLED', FALSE);

/**
 * Default value for delay (seconds).
 */
define('BACKGROUND_BATCH_DELAY', 1);

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

/**
 * 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/config/system/batch/settings'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Settings',
    'weight' => 1,
  );
  $items['admin/config/system/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.admin.inc',
  );
  $items['admin/config/system/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,
    'theme callback' => '_system_batch_theme',
    'type' => MENU_CALLBACK,
    'file' => 'background_batch.pages.inc',
    'module' => 'background_batch',
  );
}

/**
 * Implements hook_batch_alter().
 * Steal the operation and hook into context data.
 */
function background_batch_batch_alter(&$batch) {
  if (!variable_get('background_batch_enabled', BACKGROUND_BATCH_ENABLED)) {
    return;
  }
  if ($batch['progressive'] && $batch['url'] == '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);
  }

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

/**
 * Implements hook_library().
 */
function background_batch_library() {
  $libraries = array();
  $libraries['background-process.batch'] = array(
    'title' => 'Background batch API',
    'version' => '1.0.0',
    'js' => array(
      drupal_get_path('module', 'background_batch') . '/js/batch.js' => array(
        'group' => JS_DEFAULT,
        'cache' => FALSE,
      ),
    ),
    'dependencies' => array(
      array(
        'background_batch',
        'background-process.progress',
      ),
    ),
  );
  $libraries['background-process.progress'] = array(
    'title' => 'Background batch progress',
    'version' => VERSION,
    'js' => array(
      drupal_get_path('module', 'background_batch') . '/js/progress.js' => array(
        'group' => JS_DEFAULT,
        'cache' => FALSE,
      ),
    ),
  );
  return $libraries;
}

/**
 * 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) {
  $process = BackgroundProcess::currentProcess();
  if (!$process) {
    return;
  }
  if (!$id) {
    $process
      ->keepAlive(FALSE);
    return;
  }

  // Retrieve the current state of batch from db.
  $data = db_query("SELECT batch FROM {batch} WHERE bid = :bid", array(
    ':bid' => $id,
  ), array(
    'target' => 'background_process',
  ))
    ->fetchColumn();
  if (!$data) {
    $process
      ->keepAlive(FALSE);
    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) {
    $process
      ->keepAlive(FALSE);
    return drupal_access_denied();
  }
  timer_start('background_batch_processing');
  $percentage = 0;
  $lifespan = $process
    ->getOption('batch_lifespan') * 1000;
  while ($percentage < 100) {
    $current_set =& _batch_current_set();
    $queue = _batch_queue($current_set);
    if ($queue
      ->numberOfItems() <= 0) {

      // There are no items in the queue ... so the batch is invalid!
      $current_set['count'] = 0;
      $percentage = 100;
      break;
    }
    list($percentage, $message) = _batch_process();

    // Restart background process after X miliseconds
    if (timer_read('background_batch_processing') > $lifespan) {
      break;
    }
  }
  if ($percentage >= 100) {
    $batch['finish_time'] = microtime(TRUE);
    $process
      ->setProgress(1);
    $process
      ->keepAlive(FALSE);
  }
}

/**
 * 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 = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') {
  $batch =& batch_get();
  drupal_theme_initialize();
  if (isset($batch)) {

    // Add process information
    $process_info = array(
      'current_set' => 0,
      'progressive' => TRUE,
      'url' => $url,
      'url_options' => array(),
      'source_url' => $_GET['q'],
      'redirect' => $redirect,
      'theme' => $GLOBALS['theme_key'],
      'redirect_callback' => $redirect_callback,
    );
    $batch += $process_info;

    // The batch is now completely built. Allow other modules to make changes
    // to the batch so that it is easier to reuse batch processes in other
    // environments.
    drupal_alter('batch', $batch);

    // Assign an arbitrary id: don't rely on a serial column in the 'batch'
    // table, since non-progressive batches skip database storage completely.
    $batch['id'] = db_next_id();

    // Move operations to a job queue. Non-progressive batches will use a
    // memory-based queue.
    foreach ($batch['sets'] as $key => $batch_set) {
      _batch_populate_queue($batch, $key);
    }

    // Initiate processing.
    // 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',
        ),
      )),
    ));

    // Clear the way for the drupal_goto() redirection to the batch processing
    // page, by saving and unsetting the 'destination', if there is any.
    if (isset($_GET['destination'])) {
      $batch['destination'] = $_GET['destination'];
      unset($_GET['destination']);
    }

    // Store the batch.
    db_insert('batch')
      ->fields(array(
      'bid' => $batch['id'],
      'timestamp' => REQUEST_TIME,
      'token' => drupal_get_token($batch['id']),
      'batch' => serialize($batch),
    ))
      ->execute();

    // Set the batch number in the session to guarantee that it will stay alive.
    $_SESSION['batches'][$batch['id']] = TRUE;

    // Redirect for processing.
    $function = $batch['redirect_callback'];
    if (function_exists($function)) {

      // $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id'])));
    }
  }
  die("TEST");

  #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 $process = NULL;
  public function __construct() {
    $args = func_get_args();
    return call_user_func_array(array(
      'parent',
      '__construct',
    ), $args);
  }
  public function getBatch() {
    if (!$this->batch) {
      $this->batch =& batch_get();
    }
    return $this->batch;
  }
  public function getProcess() {
    if (!$this->process) {
      $this->process = BackgroundProcess::loadByHandle('background_batch:' . $this->batch['id']);
      $this->process
        ->setProgressInterval(variable_get('background_batch_delay', BACKGROUND_BATCH_DELAY));
    }
    return $this->process;
  }

  /*
    public function __destruct() {
      $this->offsetSet('finished', 0);
    }
  */
  public static function processMessage($current_set, $finished = 0, $eta = 0) {
    $total = $current_set['total'];
    $count = $current_set['count'];
    $elapsed = $current_set['elapsed'];
    $start_time = $current_set['start'];
    $progress_message = $current_set['progress_message'];
    $current = $total - $count;
    $step = 1 / $total;
    $base = $current * $step;
    $progress = $base + $finished * $step;
    $time = microtime(TRUE);
    $elapsed = floor($time - $start_time);
    $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($eta) - floor($time)),
    );
    $message = strtr($progress_message, $values);
    return array(
      $progress,
      $message,
    );
  }

  /**
   * Override offsetSet().
   * Update progress if needed.
   */
  public function offsetSet($name, $value) {
    if ($name == 'finished' || ($name = 'message')) {
      if ($this
        ->getBatch() && $this
        ->getProcess()) {
        list($progress, $message) = self::processMessage($this->batch['sets'][$this->batch['current_set']], $value, $this->process
          ->calculateETA());
        $message .= $message && $this['message'] ? '<br/>' : '';
        $message .= $this['message'];
        if ($progress >= $this->process
          ->getProgress()) {
          $this->process
            ->setProgress($progress, $message ? $message : NULL);
        }
      }
    }
    return parent::offsetSet($name, $value);
  }

}

Functions

Namesort descending Description
background_batch_batch_alter Implements hook_batch_alter(). Steal the operation and hook into context data.
background_batch_library Implements hook_library().
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 (seconds).
BACKGROUND_BATCH_ENABLED Default value for enabled
BACKGROUND_BATCH_PROCESS_ETA Default value wether ETA information should be shown.
BACKGROUND_BATCH_PROCESS_LIFESPAN Default value for process lifespan (in seconds).

Classes

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