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.pages.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 ($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) {
if (!$id) {
return;
}
// Retrieve the current state of batch from db.
$data = db_query("SELECT batch FROM {batch} WHERE bid = :bid", array(':bid' => $id))->fetchColumn();
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.
drupal_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 = 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 the error page', 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'])));
}
}
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 = $this->batch['sets'][$this->batch['current_set']]['count'];
$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'] ? '
' : '';
$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);
}
}